说一说 this
说一说 this
一、你真的了解 this 吗
我们判断 this 一般奉承一个准则:this 指向调用函数的对象。但是这个理解个人觉得还是有点浅薄不够全面。
废话不多说,先上道题目:
let obj = {
fun: function() {
console.log(this);
}
};
obj.fun();
(obj.fun)();
(obj.fun = obj.fun)();
(false || obj.fun)();
(obj.fun, obj.fun)();
最后的输入结果是:obj, obj, window, window, window
。
看看后面三个示例,咋一看是不是觉得,fun 的调用对象不还是 obj 吗,为什么输出结果还是 window?
其实不是,比如(obj.fun = obj.fun)
里面其实是一个运算的过程,最后把 obj.fun
的引用给返回了出来,然后我们直接调用其实等于是在全局作用域下直接调用了这个函数,所以它的输出是window
。
所以 this 指向调用函数的对象 这个准则还是正确的。
但,如果从ECMAScript规范去解读这个准则,你可能会有不一样的收获。
二、从ECMAScript规范解读 this
这里我的参考资料是 冴羽大神GitHub的博客
想要深入了解的建议去阅读一下,我这里就简化一下。
首先简要介绍一下 Reference 类型,这种类型不存在于我们的 js 实际开发中,它主要用于描述 js 底层行为逻辑。
我们可以这样理解,对象的每一个属性,都有自己的 Reference (翻译成中文是引用的意思)。
我们看一下 Reference 的构成:
- base value: 属性所在的对象或 EnvironmentRecord
- referenced name: 属性名
- IsPropertyReference: 判断 base value 是不是对象
- GetBase: 返回 base value
- GetValue: 返回对象属性真正的值,但是要注意:调用 GetValue,返回的将是具体的值,而不再是一个 Reference
执行一个函数,从代码上看,可以分为两部分:()
和 ()
左边的部分 ,()
左边的部分我们叫它 MemberExpression。
根据ECMAScript规范,函数执行时生成 this 的流程:
- 计算函数
()
左边 MemberExpression 的结果赋值给 ref - 判断 ref 是不是 Reference 类型
2.1 是,并且 base value 是 普通对象,this 为 base value
2.2 是,并且 base value 是 EnvironmentRecord,this 为 undefined
2.3 不是,this 为 undefined
this 为 undefined 时,在非严格模式下,会转化成 window
所以我们发现,this 主要取决于 ref 和 base value。
下面我们就上面三种情况来用例子分析一下:
情况一:
let obj = {
fun: function() {
console.log(this);
}
};
obj.fun(); // obj
// MemberExpression 为 obj.fun,解析得到的 Reference 如下
var fooReference = {
base: obj,
name: 'fun'
};
// 所以,this 指向 base = obj
情况二:
let parentFun = function() {
let fun = function() {
console.log(this);
}
fun(); // window
}
parentFun();
// MemberExpression 为 fun,解析得到的 Reference 如下
var fooReference = {
base: EnvironmentRecord,
name: 'fun'
};
// 所以,this 为 undefined,非严格模式下转化为 window
情况三:
let obj = {
fun: function() {
console.log(this);
}
};
(obj.fun = obj.fun)(); // window
/* MemberExpression 为 (obj.fun = obj.fun),有赋值操作符,在计算的时候会使用 GetValue,前面我们说过 GetValue 返回的是具体的值,不再是 Reference。
所以,this 为 undefined,非严格模式下转化为 window
*/
总结:
- function 函数的 this 指向是在函数执行的时候进行判断的。
- 大多的情况下,我们可以根据this 指向调用函数的对象这个准则来判断 this 指向。
- 但如果一个函数的引用是被计算出来的,那么这个函数的 MemberExpression 就无法被解析成 Reference,this 就是 undefined。
三、call、apply、bind
为了应对 function 复杂多变的 this 指向,ES3提出了 call、apply 两种函数方法来显示绑定函数的 this,ES5 也提出一种 bind 方法来达到类似的效果。
下面我们复习一下这三种方法的使用。
let obj = {
a: 'A',
b: 'B'
}
let fun = function(a, b) {
console.log(this.a, this.b, a, b);
}
fun.call(obj, 'a', 'b');
fun.apply(obj, ['a', 'b']);
fun.bind(obj, 'a', 'b')();
// 上面三个都输出 A B a b
复习完毕,过。
四、new 里面的 this 绑定
new 一个实例的时候,其实也存在 this 绑定,我们可以大致这样来理解 new 一个实例的流程示意:
- 创建一个新对象 obj
obj._proto_ = 构造函数.prototype
- 将obj 绑定为构造函数的 this
- 执行构造函数
- 如果构造函数没有返回值,返回 obj
- 如果构造函数有返回值
6.1 构造函数的返回值是一个对象,返回这个对象
6.2 构造函数的返回值不是一个对象,返回 obj
五、箭头函数 this
箭头函数是 ES6 的新玩意,这种函数有点骚,上面说到的 call、apply、bind、new 对它都没作用。
《ES6标准入门》色这样介绍箭头函数的 this 的:
this 指向的固定化并不是因为箭头函数内部有绑定 this 的机制,实际原因是箭头函数根本没有自己的 this,导致内部的 this 就是外层代码块的 this。
关于箭头函数没有自己的 this 这个问题,其实可以用作用域链来解释。
我们都知道,在函数执行的时候,函数的执行上下文会被推入调用栈,然后生成函数的AO对象、函数的作用域。
正常的 function 函数,它的变量对象里存在 this 这样一个变量,而变量对象是在函数执行的时候才被创建的。所以,这也是为什么 function 函数是需要到执行的时候才能确定自己的 this。
而执行箭头函数时生成的变量对象里面没有 this 这样的一个变量,所以它需要去通过作用域链去寻找自己的 this,这也就是为什么说箭头函数的 this 其实是外层代码块的 this。
而且我们知道,一个函数的作用域链除了它本身变量对象是在函数执行的时候生成的以外,作用域链上其他的变量对象都是复制函数内部属性[[scope]]的,而[[scope]]是在函数定义的时候就生成了。这就是为什么说箭头函数的 this 在函数定义的时候就决定了。
六、总结:
this 绑定一共可以分为 5 类:
1. 默认绑定
严格模式下,默认 this 为 undefined;非严格模式下,默认 this 为 window。
let fun = function() {
console.log(this) // window
}
fun();
2. 隐式绑定
当函数引用有上下文对象时,this 会绑定到这个上下文对象。
隐式丢失:将函数引用作为参数传递时,将函数引用用于计算并返回时...都可能导致隐式丢失。
let obj = {
fun() { console.log(this); }
}
obj.fun(); // obj
(0, obj.fun)(); // window
setTimeout(obj.fun, 0); // window
3. 显式绑定
前面提到的 call、apply、bind 都是显示绑定。顺带一提,被 bind 过一次后再 bind 都是无效的。
let obj = {
fun() { console.log(this); }
};
(0, obj.fun.bind(obj))(); // obj
(0, obj.fun.bind(obj)).apply(window); // obj
setTimeout(obj.fun.bind(obj), 0); // obj
5. new 绑定
new 的优先级是高于显示绑定的。
let obj1 = {
a: 1
}
let obj2 = new (function() {
this.a = 2
// 因为 new 的优先级高于显示绑定,所以 this 没有被 bind 成 obj1,而是被 new 绑定为 obj2
}.bind(obj1));
console.log(obj1.a, obj2.a); // 1 2
6. 箭头函数绑定
let obj1 = {
fun: ()=>console.log(this)
};
let obj2 = new function() {
this.fun = ()=>console.log(this);
};
obj1.fun(); // window
obj2.fun(); // obj2