第二章: this 豁然开朗! - 一切皆有顺序

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

一切皆有顺序

如此,我们已经揭示了函数调用中的四种 this 绑定规则。你需要做的 一切 就是找到调用点然后考察哪一种规则适用于它。但是,如果调用点上有多种规则都适用呢?这些规则一定有一个优先顺序,我们下面就来展示这些规则以什么样的优先顺序实施。

很显然,默认绑定 在四种规则中优先权最低的。所以我们先把它放在一边。

隐含绑定明确绑定 哪一个更优先呢?我们来测试一下:

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj1 = {
  5. a: 2,
  6. foo: foo
  7. };
  8. var obj2 = {
  9. a: 3,
  10. foo: foo
  11. };
  12. obj1.foo(); // 2
  13. obj2.foo(); // 3
  14. obj1.foo.call( obj2 ); // 3
  15. obj2.foo.call( obj1 ); // 2

所以, 明确绑定 的优先权要高于 隐含绑定,这意味着你应当在考察 隐含绑定 之前 首先 考察 明确绑定 是否适用。

现在,我们只需要搞清楚 new 绑定 的优先级位于何处。

  1. function foo(something) {
  2. this.a = something;
  3. }
  4. var obj1 = {
  5. foo: foo
  6. };
  7. var obj2 = {};
  8. obj1.foo( 2 );
  9. console.log( obj1.a ); // 2
  10. obj1.foo.call( obj2, 3 );
  11. console.log( obj2.a ); // 3
  12. var bar = new obj1.foo( 4 );
  13. console.log( obj1.a ); // 2
  14. console.log( bar.a ); // 4

好了,new 绑定 的优先级要高于 隐含绑定。那么你觉得 new 绑定 的优先级较之于 明确绑定 是高还是低呢?

注意: newcall/apply 不能同时使用,所以 new foo.call(obj1) 是不允许的,也就是不能直接对比测试 new 绑定明确绑定。但是我们依然可以使用 硬绑定 来测试这两个规则的优先级。

在我们进入代码中探索之前,回想一下 硬绑定 物理上是如何工作的,也就是 Function.prototype.bind(..) 创建了一个新的包装函数,这个函数被硬编码为忽略它自己的 this 绑定(不管它是什么),转而手动使用我们提供的。

因此,这似乎看起来很明显,硬绑定明确绑定的一种)的优先级要比 new 绑定 高,而且不能被 new 覆盖。

我们检验一下:

  1. function foo(something) {
  2. this.a = something;
  3. }
  4. var obj1 = {};
  5. var bar = foo.bind( obj1 );
  6. bar( 2 );
  7. console.log( obj1.a ); // 2
  8. var baz = new bar( 3 );
  9. console.log( obj1.a ); // 2
  10. console.log( baz.a ); // 3

哇!bar 是硬绑定到 obj1 的,但是 new bar(3)没有 像我们期待的那样将 obj1.a 变为 3。反而,硬绑定(到 obj1)的 bar(..) 调用 可以new 所覆盖。因为 new 被实施,我们得到一个名为 baz 的新创建的对象,而且我们确实看到 baz.a 的值为 3

如果你回头看看我们的“山寨”绑定帮助函数,这很令人吃惊:

  1. function bind(fn, obj) {
  2. return function() {
  3. fn.apply( obj, arguments );
  4. };
  5. }

如果你推导这段帮助代码如何工作,会发现对于 new 操作符调用来说没有办法去像我们观察到的那样,将绑定到 obj 的硬绑定覆盖。

但是 ES5 的内建 Function.prototype.bind(..) 更加精妙,实际上十分精妙。这里是 MDN 网页上为 bind(..) 提供的(稍稍格式化后的)polyfill(低版本兼容填补工具):

  1. if (!Function.prototype.bind) {
  2. Function.prototype.bind = function(oThis) {
  3. if (typeof this !== "function") {
  4. // 可能的与 ECMAScript 5 内部的 IsCallable 函数最接近的东西,
  5. throw new TypeError( "Function.prototype.bind - what " +
  6. "is trying to be bound is not callable"
  7. );
  8. }
  9. var aArgs = Array.prototype.slice.call( arguments, 1 ),
  10. fToBind = this,
  11. fNOP = function(){},
  12. fBound = function(){
  13. return fToBind.apply(
  14. (
  15. this instanceof fNOP &&
  16. oThis ? this : oThis
  17. ),
  18. aArgs.concat( Array.prototype.slice.call( arguments ) )
  19. );
  20. }
  21. ;
  22. fNOP.prototype = this.prototype;
  23. fBound.prototype = new fNOP();
  24. return fBound;
  25. };
  26. }

注意: 就将与 new 一起使用的硬绑定函数(参照下面来看为什么这有用)而言,上面的 bind(..) polyfill 与 ES5 中内建的 bind(..) 是不同的。因为 polyfill 不能像内建工具那样,没有 .prototype 就能创建函数,这里使用了一些微妙而间接的方法来近似模拟相同的行为。如果你打算将硬绑定函数和 new 一起使用而且依赖于这个 polyfill,应当多加小心。

允许 new 进行覆盖的部分是这里:

  1. this instanceof fNOP &&
  2. oThis ? this : oThis
  3. // ... 和:
  4. fNOP.prototype = this.prototype;
  5. fBound.prototype = new fNOP();

我们不会实际深入解释这个花招儿是如何工作的(这很复杂而且超出了我们当前的讨论范围),但实质上这个工具判断硬绑定函数是否是通过 new 被调用的(导致一个新构建的对象作为它的 this),如果是,它就用那个新构建的 this 而非先前为 this 指定的 硬绑定

为什么 new 可以覆盖 硬绑定 这件事很有用?

这种行为的主要原因是,创建一个实质上忽略 this硬绑定 而预先设置一部分或所有的参数的函数(这个函数可以与 new 一起使用来构建对象)。bind(..) 的一个能力是,任何在第一个 this 绑定参数之后被传入的参数,默认地作为当前函数的标准参数(技术上这称为“局部应用(partial application)”,是一种“柯里化(currying)”)。

例如:

  1. function foo(p1,p2) {
  2. this.val = p1 + p2;
  3. }
  4. // 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
  5. // 而且反正它将会被 `new` 调用覆盖掉!
  6. var bar = foo.bind( null, "p1" );
  7. var baz = new bar( "p2" );
  8. baz.val; // p1p2

判定 this

现在,我们可以按照优先顺序来总结一下从函数调用的调用点来判定 this 的规则了。按照这个顺序来问问题,然后在第一个规则适用的地方停下。

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。

    var bar = new foo()

  2. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。

    var bar = foo.call( obj2 )

  3. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。

    var bar = obj1.foo()

  4. 否则,使用默认的 this默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。

    var bar = foo()

以上,就是理解对于普通的函数调用来说的 this 绑定规则 所需的全部。是的……几乎是全部。