书接上回,我们知道了 this 是什么为什么之后,我们还有一个问题没有解决,那就是我们如何修改 this。
先来看看几种绑定规则
默认绑定
在默认情况下,即独立函数调用的情况下,this 会被绑定到 window 或者 global(node 环境)。
window.a = 1
function foo() {
console.log(this.a)
}
foo(); // 1
这里没有使用 var,虽然 var 声明的全局变量会成为全局对象的一个属性,但是我觉得直接使用 window 会来得更直观一点,另外 let 和 const 声明的全局变量并不会成为全局对象的属性。
但是有一种情况例外,就是严格模式下,this 会被绑定到 undefined
window.a = 1
function foo() {
console.log(this.a)
}
foo(); // error
隐式绑定
当我们通过一个对象调用一个函数的时候,这个函数的 this 就会指向这个对象。
function foo() {
console.log(this.a)
}
const obj = {
a: 1,
foo
}
obj.foo() // 2
当我们通过 obj 调用 foo 时,调用点使用 obj 环境来引用函数,因为 obj 是 foo() 调用的 this,所以这个时候的 this 会等于 obj:
const obj = {
a: 1,
foo
}
function foo() {
console.log(this === obj)
}
obj.foo() // true
绑定丢失
关于绑定丢失问题,就关系到引用对象的实质了。
还是以这段代码为例:
function foo() {
console.log(this.a)
}
const obj = {
a: 1,
foo
}
const obj2 = {
a: 2,
bar: function() {
console.log(this.a)
}
}
obj.foo() // 2
无论函数是声明后作为引用属性添加到对象上,还是直接在对象上声明,这个函数都不属于这个对象,对象上仅仅只是保存一个引用地址。
那么,这段代码就可以解释了:
window.a = 'in window'
function foo() {
console.log(this.a)
}
const obj = {
a: 'in obj',
foo
}
obj.foo() // 'in obj'
const bar = obj.foo // {1}
bar() // 'in window'
上面我们已经知道了 obj 上的 foo 保存的只是一个引用地址,我们再在 {1} 处将这个引用地址直接赋值给 bar,那么 bar 就直接指向函数 foo,这个时候执行 bar 就等于直接执行 foo,调用点来到了 window/global/undefined,this 自然就指向 window/global/undefined。
明确绑定
明确绑定与默认绑定相对,是由我们给 this 明确指明运行时的绑定对象,强制函数调用时使用某个特定对象作为 this 绑定,而不是在这个对象上放置这个函数。
明确绑定的方式有很多种,比如我们常见的 call 方法,它接受的第一个参数是用于 this 的对象,之后的参数都是传给这个函数的参数。
window.a = 'in window'
function foo() {
console.log(this.a)
}
const obj = {
a: 'in obj'
}
foo() // 'in window'
foo.call(obj) // 'in obj'
如果我们传递的是一个值类型(string、boolean、number)作为 this 绑定,那么这个值类型会被包装到它的对象类型中(分别是 new String()、new Boolean()、new Number())。这通常叫做封箱
硬绑定
由于明确绑定并不能完全解决我们函数“丢失”自己原本的 this 绑定,或者被第三方模块覆盖等问题,聪明的程序员便使用了明确绑定的一种变种来解决这个问题:
widnow.a = 10
function foo(otherNum = 0) {
console.log(this.a + otherNum)
}
function bind(fn, obj){
return function(...args) {
fn.call(obj, ...args)
}
}
const obj = {
a: 1,
}
const bar = bind(foo, obj)
bar(2) // 3
// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call(window) // 1
现如今硬绑定已经作为 ES5 的内建工具提供:Function.prototype.bing,像这样使用:
function foo(otherNum = 0) {
console.log(this.a + otherNum)
}
const obj = {
a: 1,
}
const bar = foo.bind(obj)
bar(2) // 3
bind(..) 返回一个硬编码的新函数,它使用你指定的 this 环境来调用原本的函数。
在 ES6 中,
bind(..)生成的硬绑定函数有一个名为.name的属性,它源自于原始的 目标函数(target function) 。举例来说:bar = foo.bind(..)应该会有一个bar.name属性,它的值为"bound foo",这个值应当会显示在调用栈轨迹的函数调用名称中。
new 绑定
先说明哈,JS 只是恰巧拥有 new 操作符,实际上 JS 的机制与 new 在面向类语言的功能并没有任何联系。
在 JS 中,构造器仅仅只是一个函数,它们偶然地与前置的 new 操作符一起调用。他们不依附于类,也不初始化一个类,甚至不是一种特殊的函数类型。他们本质上只是一般的函数,在被 new 调用时改变了行为。
当在函数前面被加入 new 调用时,也就是被构造器调用时,会自动完成下面的操作:
- 一个全新的对象被凭空创建
- 这个新创建的对象会被接入原型链([[Prototype]] - linked)
- 这个新构建的对象被设置为函数调用的 this 绑定
- 除非函数返回他自己的其他对象,否则这个被 new 调用的函数将自动返回这个新构建的对象
function foo(a) {
this.a = a
}
const bar = new foo(2)
console.log(bar.a) // 2
优先级
首先默认规则的优先权最低,因为默认规则的定义就是当没有其他规则匹配时使用默认规则。
那么我们就只需要明确剩下三个规则的优先级了。
思考以下代码:
function foo() {
console.log(this.a)
}
const obj = {
a: 'in obj',
foo
}
const obj2 = {
a: 'in obj2'
}
obj.foo() // 'in obj'
obj.foo.call(obj2) // 'in obj2'
由此可得,明确绑定的优先级要高于隐式绑定。
这个时候我们就能得到这个一个顺序
{1} > 明确绑定 > {2} > 隐式绑定 > {3} > 默认绑定
现在我们只需要搞清楚 new 绑定的优先级到底是 1、2、3 中的那个位置。
function foo(a) {
this.a = a
}
const obj1 = {
foo
}
const obj2 = {}
obj1.foo(2)
console.log(obj1.a) // 2
obj1.foo.call(obj2, 3)
console.log(obj2.a) // 3
const bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4
现在我们明确了 new 绑定的优先级要高于隐式绑定,也就是说我们排除了 3 这个选项。
接下来我们要确认到底时 1 还是 2 了
注意: new 和 call/apply 不能同时使用,所以 new foo.call(obj1) 是不允许的,也就是不能直接对比测试 new 绑定 和 明确绑定。但是我们依然可以使用 硬绑定 来测试这两个规则的优先级。
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
哇!bar 是硬绑定到 obj1 的,但是 new bar(3) 并 没有 像我们期待的那样将 obj1.a 变为 3。反而,硬绑定(到 obj1)的 bar(..) 调用 可以 被 new 所覆盖。因为 new 被实施,我们得到一个名为 baz 的新创建的对象,而且我们确实看到 baz.a 的值为 3。
在后面的 polyfill 中,bind 允许 new 进行覆盖的部分是这里:
this instanceof fNOP &&
oThis ? this : oThis
// ... 和:
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
那么我们可以得到最后的优先级排序
new 绑定 > 明确绑定 > 隐式绑定 > 默认绑定
确定 this
有了以上优先级的排序,我们可以很轻松的确定 this 了
- 函数是通过
new被调用的吗(new 绑定)?如果是,this就是新构建的对象。 - 函数是通过
call或apply被调用(明确绑定),甚至是隐藏在bind硬绑定 之中吗?如果是,this就是那个被明确指定的对象。 - 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,
this就是那个环境对象。 - 否则,使用默认的
this(默认绑定)。如果在strict mode下,就是undefined,否则是global对象。
bind 的 polyfill
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
// 可能的与 ECMAScript 5 内部的 IsCallable 函数最接近的东西,
throw new TypeError(
'Function.prototype.bind - what ' +
'is trying to be bound is not callable'
);
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(
this instanceof fNOP && oThis ? this : oThis,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
小 tips
为什么箭头函数不能用 new,是什么原因
new 的原理是执行原型链接和给新对象绑定 this,再给它赋值。
- 没有 prototype,就不能执行原型链接
- 箭头函数没有自己的 this,也不能通过 callbind 等改变 this 指向,就不能给新对象绑定。
参考
第二章: this 豁然开朗! · 你不知道的JavaScript@Js中文网-前端进阶资源教程 (javascriptc.com)