第二章: this 豁然开朗! - 绑定的特例

优质
小牛编辑
129浏览
2023-12-01

绑定的特例

正如通常的那样,对于“规则”总有一些 例外

在某些场景下 this 绑定会让人很吃惊,比如在你试图实施一种绑定,然而最终得到的却是 默认绑定 规则的绑定行为(见前面的内容)。

被忽略的 this

如果你传递 nullundefined 作为 callapplybindthis 绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var a = 2;
  5. foo.call( null ); // 2

为什么你会向 this 绑定故意传递像 null 这样的值?

一个很常见的做法是,使用 apply(..) 来将一个数组散开,从而作为函数调用的参数。相似地,bind(..) 可以柯里化参数(预设值),也可能非常有用。

  1. function foo(a,b) {
  2. console.log( "a:" + a + ", b:" + b );
  3. }
  4. // 将数组散开作为参数
  5. foo.apply( null, [2, 3] ); // a:2, b:3
  6. // 用 `bind(..)` 进行柯里化
  7. var bar = foo.bind( null, 2 );
  8. 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 的委托,所以它比 {} “空得更彻底”。

  1. function foo(a,b) {
  2. console.log( "a:" + a + ", b:" + b );
  3. }
  4. // 我们的 DMZ 空对象
  5. var ø = Object.create( null );
  6. // 将数组散开作为参数
  7. foo.apply( ø, [2, 3] ); // a:2, b:3
  8. // 用 `bind(..)` 进行 currying
  9. var bar = foo.bind( ø, 2 );
  10. bar( 3 ); // a:2, b:3

不仅在功能上更“安全”,ø 还会在代码风格上产生些好处,它在语义上可能会比 null 更清晰的表达“我想让 this 为空”。当然,你可以随自己喜欢来称呼你的 DMZ 对象。

间接

另外一个要注意的是,你可以(有意或无意地!)创建对函数的“间接引用(indirect reference)”,在那样的情况下,当那个函数引用被调用时,默认绑定 规则也会适用。

一个最常见的 间接引用 产生方式是通过赋值:

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var a = 2;
  5. var o = { a: 3, foo: foo };
  6. var p = { a: 4 };
  7. o.foo(); // 3
  8. (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

如果有这样的办法就好了:为 默认绑定 提供不同的默认值(不是 globalundefined),同时保持函数可以通过 隐含绑定明确绑定 技术来手动绑定 this

我们可以构建一个所谓的 软绑定 工具来模拟我们期望的行为。

  1. if (!Function.prototype.softBind) {
  2. Function.prototype.softBind = function(obj) {
  3. var fn = this,
  4. curried = [].slice.call( arguments, 1 ),
  5. bound = function bound() {
  6. return fn.apply(
  7. (!this ||
  8. (typeof window !== "undefined" &&
  9. this === window) ||
  10. (typeof global !== "undefined" &&
  11. this === global)
  12. ) ? obj : this,
  13. curried.concat.apply( curried, arguments )
  14. );
  15. };
  16. bound.prototype = Object.create( fn.prototype );
  17. return bound;
  18. };
  19. }

这里提供的 softBind(..) 工具的工作方式和 ES5 内建的 bind(..) 工具很相似,除了我们的 软绑定 行为。它用一种逻辑将指定的函数包装起来,这个逻辑在函数调用时检查 this,如果它是 globalundefined,就使用预先指定的 默认值obj),否则保持 this 不变。它也提供了可选的柯里化行为(见先前的 bind(..) 讨论)。

我们来看看它的用法:

  1. function foo() {
  2. console.log("name: " + this.name);
  3. }
  4. var obj = { name: "obj" },
  5. obj2 = { name: "obj2" },
  6. obj3 = { name: "obj3" };
  7. var fooOBJ = foo.softBind( obj );
  8. fooOBJ(); // name: obj
  9. obj2.foo = foo.softBind(obj);
  10. obj2.foo(); // name: obj2 <---- 看!!!
  11. fooOBJ.call( obj3 ); // name: obj3 <---- 看!
  12. setTimeout( obj2.foo, 10 ); // name: obj <---- 退回到软绑定

软绑定版本的 foo() 函数可以如展示的那样被手动 this 绑定到 obj2obj3,如果 默认绑定 适用时会退到 obj