第二章: this 豁然开朗! - 一切皆有顺序
一切皆有顺序
如此,我们已经揭示了函数调用中的四种 this
绑定规则。你需要做的 一切 就是找到调用点然后考察哪一种规则适用于它。但是,如果调用点上有多种规则都适用呢?这些规则一定有一个优先顺序,我们下面就来展示这些规则以什么样的优先顺序实施。
很显然,默认绑定 在四种规则中优先权最低的。所以我们先把它放在一边。
隐含绑定 和 明确绑定 哪一个更优先呢?我们来测试一下:
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
所以, 明确绑定 的优先权要高于 隐含绑定,这意味着你应当在考察 隐含绑定 之前 首先 考察 明确绑定 是否适用。
现在,我们只需要搞清楚 new 绑定 的优先级位于何处。
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
好了,new 绑定 的优先级要高于 隐含绑定。那么你觉得 new 绑定 的优先级较之于 明确绑定 是高还是低呢?
注意: new
和 call
/apply
不能同时使用,所以 new foo.call(obj1)
是不允许的,也就是不能直接对比测试 new 绑定 和 明确绑定。但是我们依然可以使用 硬绑定 来测试这两个规则的优先级。
在我们进入代码中探索之前,回想一下 硬绑定 物理上是如何工作的,也就是 Function.prototype.bind(..)
创建了一个新的包装函数,这个函数被硬编码为忽略它自己的 this
绑定(不管它是什么),转而手动使用我们提供的。
因此,这似乎看起来很明显,硬绑定(明确绑定的一种)的优先级要比 new 绑定 高,而且不能被 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
。
如果你回头看看我们的“山寨”绑定帮助函数,这很令人吃惊:
function bind(fn, obj) {
return function() {
fn.apply( obj, arguments );
};
}
如果你推导这段帮助代码如何工作,会发现对于 new
操作符调用来说没有办法去像我们观察到的那样,将绑定到 obj
的硬绑定覆盖。
但是 ES5 的内建 Function.prototype.bind(..)
更加精妙,实际上十分精妙。这里是 MDN 网页上为 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;
};
}
注意: 就将与 new
一起使用的硬绑定函数(参照下面来看为什么这有用)而言,上面的 bind(..)
polyfill 与 ES5 中内建的 bind(..)
是不同的。因为 polyfill 不能像内建工具那样,没有 .prototype
就能创建函数,这里使用了一些微妙而间接的方法来近似模拟相同的行为。如果你打算将硬绑定函数和 new
一起使用而且依赖于这个 polyfill,应当多加小心。
允许 new
进行覆盖的部分是这里:
this instanceof fNOP &&
oThis ? this : oThis
// ... 和:
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
我们不会实际深入解释这个花招儿是如何工作的(这很复杂而且超出了我们当前的讨论范围),但实质上这个工具判断硬绑定函数是否是通过 new
被调用的(导致一个新构建的对象作为它的 this
),如果是,它就用那个新构建的 this
而非先前为 this
指定的 硬绑定。
为什么 new
可以覆盖 硬绑定 这件事很有用?
这种行为的主要原因是,创建一个实质上忽略 this
的 硬绑定 而预先设置一部分或所有的参数的函数(这个函数可以与 new
一起使用来构建对象)。bind(..)
的一个能力是,任何在第一个 this
绑定参数之后被传入的参数,默认地作为当前函数的标准参数(技术上这称为“局部应用(partial application)”,是一种“柯里化(currying)”)。
例如:
function foo(p1,p2) {
this.val = p1 + p2;
}
// 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
// 而且反正它将会被 `new` 调用覆盖掉!
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
判定 this
现在,我们可以按照优先顺序来总结一下从函数调用的调用点来判定 this
的规则了。按照这个顺序来问问题,然后在第一个规则适用的地方停下。
函数是通过
new
被调用的吗(new 绑定)?如果是,this
就是新构建的对象。var bar = new foo()
函数是通过
call
或apply
被调用(明确绑定),甚至是隐藏在bind
硬绑定 之中吗?如果是,this
就是那个被明确指定的对象。var bar = foo.call( obj2 )
函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,
this
就是那个环境对象。var bar = obj1.foo()
否则,使用默认的
this
(默认绑定)。如果在strict mode
下,就是undefined
,否则是global
对象。var bar = foo()
以上,就是理解对于普通的函数调用来说的 this
绑定规则 所需的全部。是的……几乎是全部。