SmooshGate 常见问题解答

Mathias Bynens
Mathias Bynens

发生了什么混乱

针对名为 Array.prototype.flatten 的 JavaScript 语言功能的提案最终被发现与 Web 不兼容。在 Firefox Nightly 中发布该功能导致至少一个热门网站出现问题。鉴于存在问题的代码是广泛使用的 MooTools 库的一部分,因此可能有更多网站受到影响。(虽然在 2018 年,MooTools 不常用于新网站,但它曾经非常流行,并且仍存在于许多生产网站中。)

提案作者开玩笑地建议将 flatten 重命名为 smoosh,以避免兼容性问题。并非所有人都明白这是一个玩笑,一些人开始错误地认为新名称已经确定,情况很快就变得不可收拾。

Array.prototype.flatten 的作用是什么?

Array.prototype.flat(最初提议为 Array.prototype.flatten)会递归展平数组,直到达到指定的 depth(默认为 1)。

// Flatten one level:
const array = [1, [2, [3]]];
array.flat();
// → [1, 2, [3]]

// Flatten recursively until the array contains no more nested arrays:
array.flat(Infinity);
// → [1, 2, 3]

同一提案中还包含 Array.prototype.flatMap,它与 Array.prototype.map 类似,但会将结果展平为一个新数组。

[2, 3, 4].flatMap((x) => [x, x * 2]);
// → [2, 4, 3, 6, 4, 8]

MooTools 在执行什么操作导致了此问题?

MooTools 定义了自己的非标准 Array.prototype.flatten 版本:

Array.prototype.flatten = /* non-standard implementation */;

MooTools 的 flatten 实现与提议的标准不同。不过,这不是问题!当浏览器原生提供 Array.prototype.flatten 时,MooTools 会替换原生实现。这样可确保无论原生 flatten 是否可用,依赖于 MooTools 行为的代码都能按预期运行。到目前为止,一切进展顺利!

很遗憾,接下来发生了其他问题。MooTools 会将其所有自定义数组方法复制到 Elements.prototype(其中 Elements 是 MooTools 专用 API):

for (var key in Array.prototype) {
  Elements.prototype[key] = Array.prototype[key];
}

for-in 会迭代“可枚举”属性,其中不包括 Array.prototype.sort 等原生方法,但包括 Array.prototype.foo = whatever 等定期分配的属性。不过,重点是,如果您覆盖不可枚举的属性(例如 Array.prototype.sort = whatever),该属性仍会保持不可枚举状态。

目前,Array.prototype.flatten = mooToolsFlattenImplementation 会创建一个可枚举的 flatten 属性,因此该属性稍后会复制到 Elements。但是,如果浏览器提供的是 flatten 的原生版本,则它会变为不可枚举,并且不会复制到 Elements现在,所有依赖于 MooTools 的 Elements.prototype.flatten 的代码都已损坏。

虽然将原生 Array.prototype.flatten 更改为可枚举似乎可以解决此问题,但可能会导致更多兼容性问题。然后,所有依赖 for-in 来迭代数组的网站(这是一种不良做法,但确实会发生)都会突然为 flatten 属性获得额外的循环迭代。

这里更大的问题是修改内置对象。如今,扩展原生原型通常被认为是一种不良做法,因为它无法与其他库和第三方代码很好地组合。请勿修改不归您所有的对象!

为什么不直接保留现有名称,而要打破 Web 的规则?

1996 年,在 CSS 广泛普及之前,在“HTML5”成为热门词汇之前,《空中灌篮》网站正式上线。如今,该网站的运作方式与 22 年前一样。

这是怎么发生的?这些年来,是否有人一直在维护该网站,并在每次浏览器供应商发布新功能时对其进行更新?

事实证明,“不要破坏 Web”是 HTML、CSS、JavaScript 以及 Web 上广泛使用的任何其他标准的首要设计原则。如果发布新的浏览器功能导致现有网站无法正常运行,这对所有人来说都是不利的:

  • 受影响网站的访问者突然获得糟糕的用户体验;
  • 网站所有者在没有更改任何内容的情况下,网站从正常运行变为无法正常运行;
  • 由于用户发现“它在浏览器 X 中可用”后会切换浏览器,因此发布新功能的浏览器供应商会失去市场份额;
  • 一旦发现兼容性问题,其他浏览器供应商就会拒绝发布该扩展程序。功能规范与现实不符(“不过是虚构作品”),这不利于标准化流程。

当然,回想起来,MooTools 做错了 - 但破坏网络并不能惩罚他们,而是惩罚用户。这些用户不知道什么是 Moo 工具。或者,我们可以找到其他解决方案,让用户可以继续使用网站。做出这种选择很容易。

这是否意味着,无法从 Web 平台中移除糟糕的 API?

这要视具体情况而定。在极少数情况下,系统可能会从网络上移除不良功能。即使只是确定是否可以移除某项功能,也是一项非常棘手的工作,需要进行大量遥测来量化有多少网页的行为会发生变化。但如果相应功能非常不安全、对用户有害或很少使用,则可以这样做。

<applet><keygen>showModalDialog() 都是已成功从 Web 平台中移除的无效 API 的示例。

为什么不直接修复 MooTools?

修补 MooTools 以使其不再扩展内置对象是个好主意。不过,这并不能解决当前问题。即使 MooTools 发布了补丁版本,所有使用它的现有网站也必须更新,才能解决兼容性问题。

用户不能直接更新其 MooTools 副本吗?

在理想情况下,MooTools 会发布补丁,然后使用 MooTools 的每个网站都会在第二天神奇地更新。问题解决了,对吗?

很遗憾,这是不现实的。即使有人能够以某种方式识别出所有受影响的网站,设法找到每个网站的联系信息,成功与所有网站所有者联系,并说服他们全部进行更新(这可能意味着重构整个代码库),整个过程也至少需要数年时间。

请注意,其中许多网站都已过时,并且可能未得到维护。即使维护者仍然在,他们也可能不像您一样是高技能 Web 开发者。我们不能要求所有人因为网络兼容性问题而更改 8 年前的网站。

TC39 流程是如何运作的?

TC39 是负责通过 ECMAScript 标准改进 JavaScript 语言的委员会。

#SmooshGate 导致一些人认为“TC39 想要将 flatten 重命名为 smoosh”,但这只是一个内部笑话,并未向外界充分沟通。我们不会轻易做出更改提案名称等重大决策,也不会由单个人做出此类决策,更不会根据单个 GitHub 评论在短时间内做出此类决策。

TC39 采用清晰的功能提案分阶段流程。ECMAScript 提案及其任何重大更改(包括方法重命名)会在 TC39 会议期间讨论,并且需要获得整个委员会的批准才能正式发布。对于 Array.prototype.flatten,该提案已经历了多个协商阶段,一直到第 3 阶段,这表明该功能已准备好在网络浏览器中实现。在实现过程中出现其他规范问题很常见。在本例中,最重要的反馈是在尝试发布该功能后获得的:该功能在当前状态下会破坏 Web。正因如此,TC39 流程不会在浏览器发布某项功能后就结束,因为还会遇到诸如此类难以预测的问题。

TC39 以共识为基础,这意味着委员会必须就任何新更改达成一致。即使 smoosh 是一个严肃的建议,委员会成员似乎也可能会反对,而更倾向于使用 compactchain 等更常见的名称。

flatten 重命名为 smoosh(即使这不是开玩笑)从未在 TC39 会议上讨论过。因此,目前尚不清楚 TC39 对此主题的官方立场。在下次会议达成共识之前,任何人都不能代表 TC39 全体成员发言。

TC39 会议通常由背景非常多元化的人员参加:有些人拥有多年的编程语言设计经验,有些人从事浏览器或 JavaScript 引擎的工作,并且越来越多的与会者代表 JavaScript 开发者社区。

SmooshGate 最终是如何解决的?

2018 年 5 月的 TC39 会议期间,通过将 flatten 重命名为 flat,#SmooshGate 问题已得到正式解决。

Array.prototype.flatArray.prototype.flatMap 已在 V8 v6.9 和 Chrome 69 中发布。