Js中的this关键字,是个比较令人困惑的机制,起初我仔细研读了MDN里的介绍,但是终究不知其所以然,本着刨根究底的劲拜读了大神的《你不知道的Js》,感觉豁然开朗,在此分享个人的读后感。
什么是this?
它是一个在每个函数作用域中自动定义的特殊标识符关键字。
this不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。
换句话说:this实际上是在函数被调用时建立的一个绑定,它指向什么是完全由函数被调用的调用点来决定的。
当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含:
1.函数是从何处(调用栈 —— call-stack)被调用的
2.函数是如何被调用的
3.被传递了什么参数
……
这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。
调用点
调用点位于当前执行中的函数之前的调用,它是影响this绑定的唯一因素。
一张图让你轻松了解调用栈、调用点:
适用调用点的规则
在《你不知道的Js》中给出了4种规则,并且在同时满足时,它们各自的优先级。下面我来一一给大家分析:
默认绑定
最常见的是:独立函数调用,这种this规则相当于是没有其他规则适用时的默认规则。
例:
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
foo()被调用时,this指向的是全局对象。
若函数体内容在严格模式下,此处this会被设置为undefined。
例:
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: `this` is `undefined`
值得注意的是:foo() 的调用点的 strict mode 状态与this绑定无关。
例:
function foo() {
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对象仅仅在函数被调用的时间点上“拥有”或“包含”这个函数引用。
隐含绑定规则:当一个方法引用存在一个环境对象时,这个对象应当被用于这个函数调用的 this 绑定。换句话说,就是改变目标对象使它自身包含一个对函数的引用,而后使用这个函数引用属性来间接地(隐含地)将 this 绑定到该对象上。
值得注意的是,对象属性引用链的最后一层是影响调用点的。
例:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐含丢失
所谓的隐含丢失指的是:隐含绑定丢失了它的绑定,通常就意味着退回到了默认绑定。
看几个书作者给的例子,
例一:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数引用!
var a = "oops, global"; // `a` 也是一个全局对象的属性
bar(); // "oops, global"
解析:var bar = obj.foo,bar其实是引用foo本身。而调用点是bar(),因此此时 默认绑定 适用。
例二:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// `fn` 只不过 `foo` 的另一个引用
fn(); // <-- 调用点!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一个全局对象的属性
doFoo( obj.foo ); // "oops, global"
解析:参数传递仅仅是一种隐含的赋值,而传递函数是一个隐含的引用赋值,所以最终结果与之前一样。
如果接收你所传递回调的函数是语言内建的呢?没有区别,同样的结果。
例三:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一个全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
可以看出,回调函数丢掉自身的 this 绑定是十分常见的事情,甚至会被一些第三方框架覆盖。
明确绑定
相当于通过 call 和 apply 直接指明你想让 this 是什么。
例:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
虽然强制 this 指向了obj,但是并未解决回调函数丢失自身 this 绑定 等问题。
那么我们可以这样:把 foo.call( obj ) 包裹在一个函数里。给一个高大尚的命名——硬绑定
硬绑定
直接看例子:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( window ); // 2
将 foo.call( obj ) 包裹在bar的函数体里,这样,不论如何调用bar,this 永远和 obj 绑定。
其他例子:
由于硬绑定很常用,因此已作为 ES5 的内建工具提供:Function.prototype.bind。
注:在 ES6 中,bind(..) 生成的硬绑定函数有一个名为 .name 的属性,它源自于原始的 目标函数(target function)
API调用的环境
许多Js库里的函数和原生Js的函数都提供了一个可选参数,通常称之为环境。这样设计是为了确保回调函数使用特定的 this 而不必非得使用 bind(..)。
例:
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 使用 `obj` 作为 `this` 来调用 `foo(..)`
[1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome
从内部来说,几乎可以确定这种类型的函数是通过 call(..) 或 apply(..) 来使用 明确绑定 以节省你的麻烦。
new绑定
例:
function foo(a) {
this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
可以看出:我们构建了一个新的对象并把这个新对象作为 foo(..) 调用的 this,关于 new 的机制详细情况可参考我之前写的《Js的new关键字》。
this绑定规则优先级总结
通用规则
特例
规则难免会有特例。
当传递 null 或 undefined 作为 call、apply 或 bind 的 this 绑定参数时,会使用默认绑定的规则。
然而需要的注意的是:如果这样处理函数调用,当遇到不归自己管控的第三方包时,并且那些函数的确用了this引用,那么默认绑定规则意味着可能不经意引用或改变 global 对象(在浏览器中是 window)。
更安全的方式是:传递完全为空的对象(通过Object.create(null)创建)作为 call、apply 或 bind 的 this 绑定参数。
间接引用
例:
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
赋值表达式 p.foo = o.foo 的 结果值 是一个刚好指向底层函数对象的引用。如此,起作用的调用点就是 foo(),而非你期待的 p.foo() 或 o.foo()。根据上面的规则,默认绑定 适用。
ES6的箭头函数
例:
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!
代码等价于:
箭头函数从封闭它的(函数或全局)作用域采用 this 绑定。本质是使用广为人知的词法作用域来禁止了传统的 this 机制。
总结
在写这篇读后感最初的想法是打算在书作者的基础上对 this 进行总结并加入个人的想法,但是可能确实涉及的东西比较多,相互关联,因此可能并没有总结得很到位。感觉我只是把书重新读了一遍,然后筛选了一些我认为比较重要的知识点。详细的大家可以去参考大神的著作。