发生了什么混乱?
针对名为 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 年前一样。
这是怎么发生的?这些年来,是否有人一直在维护该网站,并在每次浏览器供应商发布新功能时对其进行更新?
事实证明,“不要破坏网络”是 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
是一个严肃的建议,委员会成员似乎也可能会反对,而更倾向于使用 compact
或 chain
等更常见的名称。
从 flatten
重命名为 smoosh
(即使这不是开玩笑)从未在 TC39 会议上讨论过。因此,目前尚不清楚 TC39 对此主题的官方立场。在下次会议达成共识之前,任何人都不能代表 TC39 全体成员发言。
TC39 会议通常由背景非常多元化的人员参加:有些人拥有多年的编程语言设计经验,有些人从事浏览器或 JavaScript 引擎的工作,并且越来越多的与会者代表 JavaScript 开发者社区。
SmooshGate 最终是如何解决的?
在 2018 年 5 月的 TC39 会议期间,通过将 flatten
重命名为 flat
,#SmooshGate 问题已得到正式解决。
Array.prototype.flat
和 Array.prototype.flatMap
已在 V8 v6.9 和 Chrome 69 中发布。