我真的懂this吗

上篇博客有举一个例子,我们在函数里使用的变量name是属于全局作用域下的,因为JavaScript的作用域是由词法作用域决定的,它在代码阶段就决定好了,跟函数是怎么调用的没有关系。但是在面向对象的语言中,在对象内部的方法中使用对象内部的属性是一个非常普遍的需求,JavaScript的作用域机制却对此并不支持,因此我们这时候需要this机制。
执行上下文中包含了变量环境、词法环境、外部环境,还有就是this。也就是说,每个执行上下文中都有一个this。我们主要来了解全局执行上下文中的this和函数执行上下文中的this。
全局执行上下文中的this,我们可以通过控制台打印一下,最终输出的是window对象,也就是说全局执行上下文中的this是指向window对象的。
函数执行上下文中的this,我们在一个函数内部打印this,执行函数打印的也是window对象。这可以理解为是一种缺陷,因为在实际开发中,我们并不希望函数执行上下文中的this指向全局对象,它打破了数据的边界。我们可以设计JavaScript为严格模式,这时候this的指向为怒define。我们可以通过以下几种方式来设置执行上下文中的this。

1、通过函数的call方法设置

funA.call(objB),funA函数内部的this指向了objB对象,bind、apply也可以用来改变this指向,call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,而bind方法不会。

2、通过对象调用方法来设置
1
2
3
4
5
6
7
var objA = {
name: 'able',
printName: function () {
console.log(this)
}
}
objA.printName();

当使用对象来调用起内部的一个方法时,该方法的this是指向对象本身的。也可以理解为在执行时将其转化成了objA.printName.call(objA);
要注意的是,隐式绑定有一个大坑,它容易丢失!如果我们把objA.printName赋给一个全局对象,然后在全局环境中调用这个对象,它内部的this是指向全局变量window的。我们可以用一个小诀窍来记住,隐式调用的格式一般是XXX.fn();fn()前边如果什么都没有,那它不是隐式绑定。

3、通过构造函数中设置

在JavaScript中,构造函对象数只是使用new时被调用的函数,它跟C++不一样,没有类的概念,因此任何一个函数都可以用new来调用,它不属于某个类,也不会实例化出一个类,只能称作是对于函数的构造调用。

1
2
3
4
function createA (name) {
this.name = name;
}
var objA = new createA ('able');

当执行new createA()我们可以分为以下几步。首先创建一个空对象objA,调用createA.call方法,将objA作为参数,createA的执行上下文创建时,它的this指向来objA对象。然后执行createA函数,此时createA函数执行上下文中的this指向objA对象,最后返回objA对象。

1
2
3
var objA  = { };
createA.call(objA);
return objA;

这样我们用new构建来一个新的对象,就会将新对象绑定到这个函数的this上。

绑定优先级

上边有列到好几种绑定规则,new绑定,通过call、apply、bind方式的显式绑定,在某个对象上触发函数调用的隐式绑定,还有默认绑定。它们的优先级为:
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

this的一些坑

嵌套函数的this不会从外层函数继承,我们来举个例子

1
2
3
4
5
6
7
8
9
10
11
var objA = {
name: 'able',
printThis: function () {
console.log(this);
function innerB () {
console.log(this);
}
innerB();
}
}
objA.printThis();

这个内部函数innerB中的this,很容易被理解为和其外层printThis函数的this是一致的。但执行后我们会发现,innerB中的this指向的是全局window对象,让人迷惑。
早期大家可能会在开发的时候在printThis函数中声明一个_this来保存this,然后在innerB函数中使用_this。当有了箭头函数之后,我们可以用箭头函数的特性来解决这个问题。因为箭头函数不会创建其自身的执行上下文,它的this取决于它的外部函数。关于箭头函数还有几点我们需要注意的,它不可以当作构造函数,不可以使用arguments对象,没有自己的this因此不能用call()等方法改变this指向。

总结

判断this指向流程走一遍:首先我们看函数是否在new中调用;然后是看函数是否通过call,apply调用,或者使用了bind(即硬绑定);、接下来看函数是否在某个上下文对象中调用(隐式绑定);如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。要注意的是如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。还有如果是箭头函数,箭头函数的this继承的是外层代码块的this。