this
应该是js中最为复杂的机制之一。搞懂this
某种程度上意味着一次重生。
我们先通过一段代码来看看this
有多么让人琢磨不透:
function foo(num) {
console.log(`foo: ${num}`);
this.count++;
}
foo.count = 0;
foo(1);
foo(2);
console.log(foo.count); // ?
此时的foo.count
的值是多少呢?答案是0。
显然,我们调用了两次foo
函数,所以this.count++
也肯定运行了两次。但是最后输出的this.count
却还是0。那么函数里面的this
到底实际指向的什么呢?既然代码能够正确的运行,那么this.count
到底又是哪一个count
呢?
如果我们最后console.log
加上一个断点,看看各个作用域的情况,我们会发现此时的全局作用域多了一个count
变量,并且它的值此时为NaN
。
当前的执行模式是非严格模式
this绑定规则
首先我们要抛出一个结论:函数(箭头函数除外)内部的this
是在函数被调用时绑定的,它的值取决于函数的调用方式。可以分为以下情况:
默认绑定
即独立函数调用。如果在严格模式下,this
绑定到undefined
;非严格模式下,绑定到全局对象。
global.a = 2;
function foo() {
console.log(this.a);
}
foo(); // 2
foo
此时被独立调用,在非严格模式下,this
绑定到global
,所以能输出2。如果是严格模式:
"use strict";
function foo() {
console.log(this.a);
}
foo(); // TypeError: Cannot read property 'a' of undefined
this
绑定到undefined
。所以无法获取a
变量。
现在我们再回头看看最开始的例子。因为foo
是非严格模式下的独立调用,所以this
指向全局对象。此时会在全局对象创建一个变量a
,在未赋值的情况下,a
的值是undefined
。undefined + 1
的结果就是NaN
,第二次就是Nan + 1
所以结果仍是NaN
隐式绑定
如果是通过类似于obj.fn()
的形式调用时,那么fn
内部的this
会绑定到obj
对象上。
function foo() {
console.log(this.a);
}
const obj = {
a: 2,
foo,
};
obj.foo(); // 2; 此时foo中的this会被绑定到obj对象
需要注意的是调用函数的this
会绑定到对象属性引用链只有最后一层:
function foo() {
console.log(this.a);
}
const obj2 = {
a: 2,
foo,
};
const obj1 = {
a: 1,
obj2,
};
obj1.obj2.foo(); // 2; 此时foo中的this会被绑定到obj2对象
思考以下绑定情况:
function foo() {
console.log(this.a);
}
const obj = {
a: 2,
foo,
};
const bar = obj.foo;
bar(); // undefined
我们通过将obj.foo
赋值给bar
,然后调用bar
。此时bar
函数属于独立调用,所以会使用默认绑定规则。
显示绑定
也称为硬绑定,即使用call,
apply或者bind
的方式显示指定this
所绑定的对象。
function foo() {
console.log(this.a);
}
const obj = {
a: 2,
};
foo.call(obj); // 显示绑定this为obj
需要注意:如果把undefined
或者null
作为硬绑定的this
对象。这些值会被忽略,最终还是应用默认绑定规则。
绑定优先级
不同的绑定方式优先级:显示绑定 > 隐式绑定 > 默认绑定
function foo() {
console.log(this.a);
}
const obj = {
a: 2,
foo,
};
obj.foo(); // 2
obj.foo.call({ a: 3 }); // 3
箭头函数
箭头函数不会按照以上的三种规则绑定this
,而是根据外层作用域来决定this
。即当箭头函数被声明时,它所在的作用域的this
是什么,那么箭头函数内部的this
就是什么。并且不会再发生改变(这就是为什么箭头函数无法作为构造函数)。
function foo() {
return () => {
console.log(this.a);
};
}
const obj = { a: 2 };
const bar = foo.call(obj);
bar.call({ a: 3 }); // 2
在调用foo
函数时,会在它内部声明一个箭头函数,而通过foo.call({ a: 2 })
的形式调用函数,那么foo
内部的this
绑定为obj
。此时声明的箭头函数会捕获此时的this
。所以箭头函数内部的this
也会绑定到obj
。并且箭头函数的this
无法被修改,所以后续对箭头函数的调用,其内部使用的this
始终是obj
。
箭头函数常用于回调函数:
function foo() {
setTimeout(() => {
console.log(this.a); // this继承于foo
}, 1000);
}
foo.call({ a: 2 }); // 2
如果我们将箭头函数替换为普通的匿名函数,结果将会为undefined
。因为setTimeout
的回调函数在真正调用时其实是独立调用,所以会应用默认绑定。而箭头函数在执行foo
时就已经确定是{a: 2}
这个对象,不会再修改。