Thenable鸭子类型(Duck Typing)

在Promise的世界中,一个重要的细节是如何确定一个值是否是纯粹的Promise。或者更直接地说,一个值会不会像Promise那样动作?

我们知道Promise是由new Promise(..)语法构建的,你可能会想p instanceof Promise将是一个可以接受的检查。但不幸的是,有几个理由表明它不是完全够用。

主要原因是,你可以从其他浏览器窗口中收到Promise值(iframe等),其他的浏览器窗口会拥有自己的不同于当前窗口/frame的Promise,这种检查将会在定位Promise实例时失效。

另外,一个库或框架可能会选择实现自己的Promise而不是用ES6原生的Promise实现。事实上,你很可能在根本没有Promise的老版本浏览器中通过一个库来使用Promise。

当我们在本章稍后讨论Promise的解析过程时,为什么识别并同化一个非纯种但相似Promise的值仍然很重要会愈发明显。但目前只需要相信我,它是拼图中很重要的一块。

如此,人们决定识别一个Promise(或像Promise一样动作的某些东西)的方法是定义一种称为“thenable”的东西,也就是任何拥有then(..)方法的对象或函数。这种方法假定任何这样的值都是一个符合Promise的thenable。

根据值的形状(存在什么属性)来推测它的“类型”的“类型检查”有一个一般的名称,称为“鸭子类型检查”——“如果它看起来像一只鸭子,并且叫起来像一只鸭子,那么它一定是一只鸭子”(参见本丛书的 类型与文法)。所以对thenable的鸭子类型检查可能大致是这样:

  1. if (
  2. p !== null &&
  3. (
  4. typeof p === "object" ||
  5. typeof p === "function"
  6. ) &&
  7. typeof p.then === "function"
  8. ) {
  9. // 认为它是一个thenable!
  10. }
  11. else {
  12. // 不是一个thenable
  13. }

晕!先把将这种逻辑在各种地方实现有点丑陋的事实放在一边不谈,这里还有更多更深层的麻烦。

如果你试着用一个偶然拥有then(..)函数的任意对象/函数来完成一个Promise,但你又没想把它当做一个Promise/thenable来对待,你的运气就用光了,因为它会被自动地识别为一个thenable并以特殊的规则来对待(见本章后面的部分)。

如果你不知道一个值上面拥有then(..)就更是这样。比如:

  1. var o = { then: function(){} };
  2. // 使`v`用`[[Prototype]]`链接到`o`
  3. var v = Object.create( o );
  4. v.someStuff = "cool";
  5. v.otherStuff = "not so cool";
  6. v.hasOwnProperty( "then" ); // false

v看起来根本不像是一个Promise或thenable。它只是一个拥有一些属性的直白的对象。你可能只是想要把这个值像其他对象那样传递而已。

但你不知道的是,v[[Prototype]]连接着(见本丛书的 this与对象原型)另一个对象o,在它上面偶然拥有一个then(..)。所以thenable鸭子类型检查将会认为并假定v是一个thenable。噢。

它甚至不需要直接故意那么做:

  1. Object.prototype.then = function(){};
  2. Array.prototype.then = function(){};
  3. var v1 = { hello: "world" };
  4. var v2 = [ "Hello", "World" ];

v1v2都将被假定为是thenalbe的。你不能控制或预测是否有其他代码偶然或恶意地将then(..)加到Object.prototypeArray.prototype,或其他任何原生原型上。而且如果这个指定的函数并不将它的任何参数作为回调调用,那么任何用这样的值被解析的Promise都将无声地永远挂起!疯狂。

听起来难以置信或不太可能?也许。

要知道,在ES6之前就有几种广为人知的非Promise库在社区中存在了,而且它们已经偶然拥有了称为then(..)的方法。这些库中的一些选择了重命名它们自己的方法来回避冲突(这很烂!)。另一些则因为它们无法改变来回避冲突,简单地降级为“不兼容基于Promise的代码”的不幸状态。

用来劫持原先非保留的——而且听起来完全是通用的——then属性名称的标准决议是,没有值(或它的任何委托),无论是过去,现在,还是将来,可以拥有then(..)函数,不管是有意的还是偶然的,否则这个值将在Promise系统中被混淆为一个thenable,从而可能产生非常难以追踪的Bug。

警告: 我不喜欢我们用thenable的鸭子类型来结束对Promise认知的方式。还有其他的选项,比如“branding”或者甚至是“anti-branding”;我们得到的似乎是一个最差劲儿的妥协。但它并不全是悲观与失望。thenable鸭子类型可以很有用,就像我们马上要看到的。只是要小心,如果thenable鸭子类型将不是Promise的东西误认为是Promise,它就可能成为灾难。