【你不知道的JS】-- 理解 this
一、为什么要用 this
便于隐式‘传递’一个对象的引用
二、对于 this 的两个误区
- this 指向自身
- this 指向它的作用域
function foo() {
var a = 2;
this.bar(); // 这里用 this.bar() 或 bar() 都一样
}
function bar() {
console.log(this.a);
}
foo(); // ReferenceError: a is not defined
上述代码中,企图在 bar 函数内部访问 foo 函数内部定义的变量 a ,但是失败了。究其原因, bar 函数内部的 this 实际指向了 window 对象,而不是 foo 对象。
三、this 到底是什么
this 是在运行时进行绑定的,并不是在函数声明时绑定的。 this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(调用位置和调用方法)。
- 如何找到函数的调用位置?
通过层层分析函数的调用栈,来找到函数的调用位置
四、this 的绑定规则
- 默认绑定
默认绑定适用于最常用的函数调用类型:独立函数调用。可以将这条规则看作是无法应用其他规则时的默认规则。即当函数是直接使用不带任何修饰的函数引用进行调用时,只能使用默认绑定。
注意:只有函数本身运行在严格模式下时,this 是无法绑定到全局对象上的,因此 this 只能绑定到 undefined。
即对于默认绑定来说,决定 this 绑定对象的不是调用位置是否处于严格模式,二是函数体是否处于严格模式。
具体的区别可以查看一下两段代码:
// 函数本身使用严格模式 --- 无法绑定到全局对象
function foo() {
"use strict";
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
// 函数本身并非严格模式,但是调用位置使用严格模式 --- 可以绑定到全局对象
function foo() {
console.log(this.a);
}
function bar() {
console.log(this.a);
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})()
- 隐式绑定
隐式绑定规则需要考虑函数的调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
上述代码中,无论是直接在 obj 中定义 foo 函数,函数先定义函数再添加为引用属性,这个函数严格来说都不属于 obj 对象。但是在调用时,使用了 obj 作为上下文来引用了函数(obj.foo()),因此可以说函数在调用时被 obj 对象拥有或者包含。
链式调用时,对象的属性引用链中只有最后一层会影响调用位置。(或者说最靠近函数的对象会影响调用位置)
例如:obj1.obj2.obj3.foo()
中,foo 中的 this 指向 obj3 对象。
隐式丢失:被隐式绑定的对象会丢失绑定对象,这种情况会使用默认绑定。
隐式丢失的两种常见情况:1. 函数别名;2. 函数当做回调函数作为参数传递给另一函数,因为参数传递就是一种隐式赋值,也相当于在赋值中使用了函数别名。
- 显式绑定
显示绑定是指使用 call(…)、apply(…)、bind(…) 方法在某个对象上强制调用函数。
- 硬绑定。其中,bind(…) 方法是硬绑定,硬绑定的函数是不可能被再次修改 this 指向的。具体的实现可以参考如下代码,即使用 call(…) 或 apply(…) 绑定的函数外面再用函数包裹一层,这样能够确保函数在预期的上下文环境中被调用:
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
// 无法修改 this 指向
bar.call(window); // 2
- API 调用的上下文。JavaScript中许多内置的API函数都允许传入一个指定的上下文对象来确保回调函数的 this 指向。以 forEach 为例:
function foo(el) {
console.log(el, this.id);
}
var obj = {
id: "awesome"
};
var bar = function() {
foo.call(obj);
}
// 调用 foo(...) 时把 this 绑定到 obj
[1, 2, 3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome
// 无法修改 this 指向
bar.call(window); // 2
- new 绑定
实际上不存在一类特殊的“构造函数”,只有对于函数的“构造调用”。使用 new 关键字调用函数以进行 this 绑定的方法称为 new 绑定。
使用 new 来调用函数时,会自动依次执行下列操作:
- 传建一个新的对象;
- 这个对象会被执行[[原型]]连接;
- 这个新对象回被绑定到函数调用的 this;
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
- 上述四种规则的优先级
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
五、this 绑定规则的例外
- 被忽略的 this。
如果把 null 或者 undefined 作为 this 的绑定对象传入 call、aplly 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。如果希望函数绑定一个空对象,最简单的方法是使用 “DMZ” 对象,即Object.create(null)
。Object.create(null)
和{}
很像,但是不会创建Object.prototype
这个委托,所以它比{}
更空。 - 间接引用。
开发中你可能会有意或者无意地创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生,如下例所示:
function foo() {
console.log(this.a);
}
var a = 2;
var o = {
a: 3,
foo: foo
};
o.foo(); // 3
(p.f00 = o.foo)(); // 2
赋值表达式 p.foo = o.foo
的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。 所以这里应该使用默认绑定。
3. 软绑定。
硬绑定是指 this 绑定对象之后无法对绑定的对象进行修改,顾名思义,软绑定就是希望 this 绑定对象之后依然能够保留隐式绑定或者显示绑定来修改 this 绑定的能力,从而增加函数的灵活性。
六、特殊的箭头函数
箭头函数不使用上述的四种规则进行 this 绑定,而是根据外层(函数或者全局)作用域来决定 this。
通过如下示例看看箭头函数的词法作用域:
function foo() {
// 返回一个箭头函数
return (a) => {
// this 继承自 foo()
console.log(this.a);
};
}
var obj1 = { a: 2 };
var obj2 = { a: 3 };
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是3!
foo() 内部的箭头函数会捕获调用时 foo() 的 this 。由于 foo() 的 this 绑定到 obj1,所以 bar(箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)
最常见的,箭头函数作为回调函数使用:
function foo() {
setTimeout(() => {
// 这里的 this 在词法上继承自 foo()
console.log(this.a);
}, 100);
}
var obj = { a: 2 };
foo.call(obj); // 2
箭头函数可以像 bind(…) 一样确保函数的 this 被绑定到指定对象。
实际在箭头函数出现之前就已经使用过一种几乎和箭头函数完全一样的模式。
function foo() {
var self = this;
setTimeout(function() {
console.log(self.a);
}, 100);
}
var obj = { a: 2 };
foo.call(obj); // 2
上述代码使用 self = this
这种词法作用域取代了 this 机制,达到了箭头函数的效果。
参考资料
- 《你不知道的JavaScript》