5

JavaScript中this关键字

 3 years ago
source link: https://zhuanlan.zhihu.com/p/30229111
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JavaScript中this关键字

this一直是js中一个老生常谈的东西,但是我们究竟该如何来理解它呢?
在《JavaScript高级程序设计》中,对this的解释是:

this对象是在运行时基于函数的执行环境绑定的。

我们来逐字解读这句话:

  • this是一个对象
  • this的产生与函数有关
  • this与执行环境绑定

说通俗一点就是,“谁调用的这个函数,this就是谁”。


一、函数直接调用中的this

举个栗子:

var x = 1;

function testThis() {
    console.log(this.x);
}

testThis();  //1

js中有一个全局对象window,直接调用函数testThis时,就相当于调用window下的testThis方法,包括直接声明的变量也都是挂载在window对象下的。

var x = 1;
function testThis() {
    this.innerX = 10;
    return 1;
}
testThis() === window.testThis();  // true
innerX === window.innerX;  // true
x === window.x;  // true

同理,在匿名函数中使用this也是指向的window,因为匿名函数的执行环境具有全局性。

(function () {
    console.log(this); //window
})();

但是呢,凡事都有例外,js的例外就是严格模式。在严格模式中,禁止this关键字指向全局对象。

(function () {
    'use strict';
    console.log(this); //undefined
})();

二、对象方法调用中的this

再举个栗子:

var person = {
    "name": "shenfq",
    "showName": function () {
        console.log(this.name);
    } 
};

person.showName(); // 'shenfq'

此时,showName方法中的this指向的是对象person,因为调用showName的是person对象,所以showName方法中的 this.name 其实就是 person.name。

但是如果我们换个思路,把showName方法赋值给一个全局变量,然后在全局环境下调用。

var name = 'global',
    person = {
        "name": "shenfq",
        "showName": function () {
            console.log(this.name);
        } 
    },
    showGlobalName = person.showName;
showGlobalName(); // 'global'

可以看到,在全局环境中调用showName方法时,this就会指向window。

再换个思路,如果showName方法被其他对象调用呢?

var person = {
        "name": "shenfq",
        "showName": function () {
            console.log(this.name);
        } 
    },
    animal = {
        "name": "dog",
        "showName": person.showName
    };

animal.showName(); // 'dog'

此时的name又变成了animal对象下的name,再复杂一点,如果调用方法的是对象下的一个属性,而这个属性是另个对象。

function showName () {
    console.log(this.name);
}
var person = {
    "name": "shenfq",
    "bodyParts": {
        "name": "hand",
        "showName": showName
    },
    "showName": showName
};

person.showName(); // 'shenfq'
person.bodyParts.showName(); // 'hand'

虽然调用showName方法的最源头是person对象,但是最终调用的是person下的bodyParts,所以方法写在哪个对象下其实不重要,重要的是这个方法最后被谁调用了,this指向的永远是最终调用它的那个对象。讲来讲去,this也就那么回事,只要知道函数体的执行上下文就能知道this指向哪儿,这个规则在大多数情况下都适用,注意是大多数情况,少部分情况后面会讲。

最后一个思考题,当方法返回一个匿名函数,这个匿名函数里面的this指向哪里?

var name = 'global',
    person = {
        "name": "shenfq",
        "returnShowName": function () {
            return function () {
                console.log(this.name);
            }
        } 
    };

person.returnShowName()(); // 'global'

答案一目了然,匿名函数不管写在哪里,只要是被直接调用,它的this都是指向window,因为匿名函数的执行环境具有全局性。


三、new构造函数中的this

还是先举个栗子:

function Person (name) {
    this.name = name;
}
var global = Peson('global'),
    xiaoming = new Person('xiaoming');

console.log(window.name); // 'global'
console.log(xiaoming.name); // 'xiaoming'

首先不使用new操作符,直接调用Person函数,这时的this任然指向window。当使用了new操作符时,这个函数就被称为构造函数。

所谓构造函数,就是用来构造一个对象的函数。构造函数总是与new操作符一起出现的,当没有new操作符时,该函数与普通函数无区别。

对构造函数进行new操作的过程被称为实例化。new操作会返回一个被实例化的对象,而构造函数中的this指向的就是那个被实例化的对象,比如上面例子中的xiaoming。

关于构造函数有几点需要注意:

  1. 实例化对象默认会有constructor属性,指向构造函数;
function Person (name) {
    this.name = name;
}
var xiaoming = new Person('xiaoming');

console.log(xiaoming.constructor); // Person
  1. 实例化对象会继承构造函数的原型,可以调用构造函数原型上的所有方法;
function Person (name) {
    this.name = name;
}
Person.prototype = {
    showName: function () {
        console.log(this.name);
    }
};
var xiaoming = new Person('xiaoming');

xiaoming.showName(); // 'xiaoming'
  1. 如果构造函数返回了一个对象,那么实例对象就是返回的对象,所有通过this赋值的属性都将不存在
function Person (name, age) {
    this.name = name;
    this.age  = age;
    return {
        name: 'innerName'
    };
}
Person.prototype = {
    showName: function () {
        console.log(this.name);
    }
};
var xiaoming = new Person('xiaoming', 18);

console.log(xiaoming); // {name: 'innerName'}

四、通过call、apply间接调用函数时的this

又一次举个栗子:

var obj = {
    "name": "object"
}

function test () {
    console.log(this.name);
}

test.call(obj);   // 'object'
test.apply(obj);  // 'object'

callapply方法都是挂载在Function原型下的方法,所有的函数都能使用。

这两个函数既有相同之处也有不同之处:

  • 相同的地方就是它们的第一个参数会绑定到函数体的this上,如果不传参数,this默认还是绑定到window上。
  • 不同之处在于,call的后续参数会传递给调用函数作为参数,而apply的第二个参数为一个数组,数组里的元素就是调用函数的参数。

语言很苍白,我只好写段代码:

var person = {
    "name": "shenfq"
};
function changeJob(company, work) {
    this.company = company;
    this.work    = work;
};

changeJob.call(person, 'NASA', 'spaceman');
console.log(person.work); // 'spaceman'

changeJob.apply(person, ['Temple', 'monk']);
console.log(person.work); // 'monk'

有一点值得注意,这两个方法会把传入的参数转成对象类型,不管传入的字符串还是数字。

var number = 1, string = 'string';
function getThisType () {
    console.log(typeof this);
}

getThisType.call(number); //object
getThisType.apply(string); //object

五、通过bind改变函数的this指向

最后举个栗子:

var name = 'global',
    person = {
        "name": "shenfq"
    };
function test () {
    console.log(this.name);
}

test(); // global

var newTest = test.bind(person);
newTest(); // shenfq

bind方法是ES5中新增的,和call、apply一样都是Function对象原型下的方法– Function.prototype.bind ,所以每个函数都能直接调用。bind方法会返回一个与调用函数一样的函数,只是返回的函数内的this被永久绑定为bind方法的第一个参数,并且被bind绑定后的函数不能再被重新绑定。

function showName () {
    console.log(this.name);
}
var person = {"name": "shenfq"},
    animal = {"name": "dog"};

var showPersonName = showName.bind(person),
    showAnimalName = showPersonName.bind(animal);

showPersonName(); //'shenfq'
showAnimalName(); //'shenfq'

可以看到showPersonName方法先是对showName绑定了person对象,然后再对showPersonName重新绑定animal对象并没有生效。

六、箭头函数中的this

真的是最后一个栗子:

var person = {
    "name": "shenfq",
    "returnArrow": function () {
        return () => {
            console.log(this.name);
        }
    }
};

person.returnArrow()(); // 'shenfq'

箭头函数是ES6中新增的一种语法糖,简单说就是匿名函数的简写,但是与匿名函数不同的是箭头函数中的this表示的是外层执行上下文,也就是说箭头函数的this就是外层函数的this。

var person = {
    "name": "shenfq",
    "returnArrow": function () {
        let that = this;
        return () => {
            console.log(this == that);
        }
    }
};

person.returnArrow()(); // true

补充:

事件处理函数中的this:

var $btn = document.getElementById('btn');
function showThis () {
    console.log(this);
}
$btn.addEventListener('click', showThis, false);

点击按钮可以看到控制台打印出了元素节点。

其实事件函数中的this默认就是绑定事件的元素,调用事件函数时可以简单理解为

$btn.showThis()

只要单击了按钮就会已这种方式来触发事件函数,所以事件函数中的this表示元素节点,这也与之前定义的“谁调用的这个函数,this就是谁”相吻合。

eval中的this:

eval('console.log(this)'); //window
var obj = {
    name: 'object',
    showThis: function () {
        eval('console.log(this)');
    }
}
obj.showThis(); // obj

eval是一个可以动态执行js代码的函数,能将传入其中的字符串当作js代码执行。这个方法一般用得比较少,因为很危险,想想动态执行代码,什么字符串都能执行,但是如果用得好也能带来很大的便利。

eval中的this与箭头函数比较类似,与外层函数的this一致。

当然这只针对现代浏览器,在一些低版本的浏览器上,比如ie7、低版本webkit,eval的this指向会有些不同。

eval也可以在一些特殊情况下用来获取全局对象(window、global),使用 (1,eval)(‘this’)


先写这么多,有需要再补充 ^ _ ^

参考:


博客链接


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK