第二章: this 豁然开朗! - 绑定的特例
绑定的特例
正如通常的那样,对于“规则”总有一些 例外。
在某些场景下 this
绑定会让人很吃惊,比如在你试图实施一种绑定,然而最终得到的却是 默认绑定 规则的绑定行为(见前面的内容)。
被忽略的 this
如果你传递 null
或 undefined
作为 call
、apply
或 bind
的 this
绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
为什么你会向 this
绑定故意传递像 null
这样的值?
一个很常见的做法是,使用 apply(..)
来将一个数组散开,从而作为函数调用的参数。相似地,bind(..)
可以柯里化参数(预设值),也可能非常有用。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 将数组散开作为参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
这两种工具都要求第一个参数是 this
绑定。如果目标函数不关心 this
,你就需要一个占位值,而且正如这个代码段中展示的,null
看起来是一个合理的选择。
注意: 虽然我们在这本书中没有涵盖,但是 ES6 中有一个扩散操作符:...
,它让你无需使用 apply(..)
而在语法上将一个数组“散开”作为参数,比如 foo(...[1,2])
表示 foo(1,2)
—— 如果 this
绑定没有必要,可以在语法上回避它。不幸的是,柯里化在 ES6 中没有语法上的替代品,所以 bind(..)
调用的 this
参数依然需要注意。
可是,在你不关心 this
绑定而一直使用 null
的时候,有些潜在的“危险”。如果你这样处理一些函数调用(比如,不归你管控的第三方包),而且那些函数确实使用了 this
引用,那么 默认绑定 规则意味着它可能会不经意间引用(或者改变,更糟糕!)global
对象(在浏览器中是 window
)。
很显然,这样的陷阱会导致多种 非常难 诊断和追踪的 Bug。
更安全的 this
也许某些“更安全”的做法是:为了 this
而传递一个特殊创建好的对象,这个对象保证不会对你的程序产生副作用。从网络学(或军事)上借用一个词,我们可以建立一个“DMZ”(非军事区)对象 —— 只不过是一个完全为空,没有委托(见第五,六章)的对象。
如果我们为了忽略自己认为不用关心的 this
绑定,而总是传递一个 DMZ 对象,那么我们就可以确定任何对 this
的隐藏或意外的使用将会被限制在这个空对象中,也就是说这个对象将 global
对象和副作用隔离开来。
因为这个对象是完全为空的,我个人喜欢给它一个变量名为 ø
(空集合的数学符号的小写)。在许多键盘上(比如 Mac 的美式键盘),这个符号可以很容易地用 ⌥
+o
(option+o
)打出来。有些系统还允许你为某个特殊符号设置快捷键。如果你不喜欢 ø
符号,或者你的键盘没那么好打,你当然可以叫它任意你希望的名字。
无论你叫它什么,创建 完全为空的对象 的最简单方法就是 Object.create(null)
(见第五章)。Object.create(null)
和 {}
很相似,但是没有指向 Object.prototype
的委托,所以它比 {}
“空得更彻底”。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );
// 将数组散开作为参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 进行 currying
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
不仅在功能上更“安全”,ø
还会在代码风格上产生些好处,它在语义上可能会比 null
更清晰的表达“我想让 this
为空”。当然,你可以随自己喜欢来称呼你的 DMZ 对象。
间接
另外一个要注意的是,你可以(有意或无意地!)创建对函数的“间接引用(indirect reference)”,在那样的情况下,当那个函数引用被调用时,默认绑定 规则也会适用。
一个最常见的 间接引用 产生方式是通过赋值:
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()
。根据上面的规则,默认绑定 适用。
提醒: 无论你如何得到适用 默认绑定 的函数调用,被调用函数的 内容 的 strict mode
状态 —— 而非函数的调用点 —— 决定了 this
引用的值:不是 global
对象(在非 strict mode
下),就是 undefined
(在 strict mode
下)。
软化绑定(Softening Binding)
我们之前看到 硬绑定 是一种通过将函数强制绑定到特定的 this
上,来防止函数调用在不经意间退回到 默认绑定 的策略(除非你用 new
去覆盖它!)。问题是,硬绑定 极大地降低了函数的灵活性,阻止我们手动使用 隐含绑定 或后续的 明确绑定 来覆盖 this
。
如果有这样的办法就好了:为 默认绑定 提供不同的默认值(不是 global
或 undefined
),同时保持函数可以通过 隐含绑定 或 明确绑定 技术来手动绑定 this
。
我们可以构建一个所谓的 软绑定 工具来模拟我们期望的行为。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this,
curried = [].slice.call( arguments, 1 ),
bound = function bound() {
return fn.apply(
(!this ||
(typeof window !== "undefined" &&
this === window) ||
(typeof global !== "undefined" &&
this === global)
) ? obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
这里提供的 softBind(..)
工具的工作方式和 ES5 内建的 bind(..)
工具很相似,除了我们的 软绑定 行为。它用一种逻辑将指定的函数包装起来,这个逻辑在函数调用时检查 this
,如果它是 global
或 undefined
,就使用预先指定的 默认值 (obj
),否则保持 this
不变。它也提供了可选的柯里化行为(见先前的 bind(..)
讨论)。
我们来看看它的用法:
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 ); // name: obj <---- 退回到软绑定
软绑定版本的 foo()
函数可以如展示的那样被手动 this
绑定到 obj2
或 obj3
,如果 默认绑定 适用时会退到 obj
。