原问题:
ES6 的 Symbol 为什么还有 Symbol.unscopables 这个内置方法? ES6 的 module 不是默认严格模式嘛?这样就不能用 with 了。
以下是回答备份:
这个方法最早引入是在 2013-07-23 的 TC39 会议上,为了解决 Array.prototype.values
的兼容性问题。
如果懒得看冗长的会议纪要的话,我可以解释一下:
首先,默认严格模式的是 ES modules,但是 ES6 语法不一定非要在 ES modules 中执行。
比如在浏览器中,只有 <script module>
是在 ES Modules 环境下执行的,只有这时候才是默认严格模式。
其他用 <script>
引入的脚本都仍然是传统的执行环境,严格模式是要手动开启的(所以,ES module 代码被 babel 转译后开头都会多一行 'use strict'
,以确保语义的一致性)。
但是在这些环境中新的 ES 语法特性还是可以启用的。
这样一来,TC39 在往 ES 语法里加新特性时就会碰到一些非常让人头疼的兼容性问题,尤其是往原型链上加的新特性。
第一类问题是当年的老牌前端框架都喜欢改原生对象的原型链。
比如 MooTools 就往 Array.prototype
上加了一个 contains
方法,导致后来 TC39 再想往标准里加这个方法时,不得不改名为 includes
以保证兼容性(Bug report)。
String.prototype.contains
也经历了类似的改名过程。
还有就是 Array.prototype.flatten
所经历的「smoosh 门 」事件。
这类问题没什么好的解决办法,本来 TC39 往原型链上加方法时就已经慎之又慎了,新方法都是不可枚举的以防破坏任何 for…in 代码,结果 MooTools 还刚好因为这个不可枚举的特性撞上枪口……
所以碰到这种问题的新特性,最后下场都是一直改名直到改到跟网上现存代码没什么大的兼容性问题为止。
第二类问题是新的原型链方法在 with
环境 中的兼容性问题。考虑这段代码:
// 代码引自 Exploring ES6
function foo(values) {
with (values) {
console.log(values.length); // 预计输出 0,实际输出 abc
}
}
Array.prototype.values = { length: 'abc' };
foo([]);
with (values)
的作用是,在这段代码之中,存在于 values
对象之中的字段都可以省略 values.
前缀访问,但如果加了前缀,理论上也应该不会有什么问题 —— 最多也就是浪费了 with
的特殊功能没去用嘛。
直到 TC39 往数组原型链上加了 .values
方法。
这时候,console.log(values.length)
实际是被当成了 console.log(values.values.length)
执行了,因此行为与原先预计的不再一致。这就是破坏了 Web 兼容性的行为。
也不要以为这种匪夷所思的代码只有新手程序员才会写出来 —— 这个问题是被知名框架 Ext.js v4.2.1 暴露出来的。
由于 Ext.js 使用得实在是太广泛了,TC39 不得不又再次寻找应对的办法……
好在这次的问题场景还是比较局限的。既然是在 with
执行环境下出的问题,我们就往 with
环境里再加个规则,让它不要读取原型链上的某些方法名就好了。 所以就有了 Symbol.unscopables
。
跟用 Symbol.iterator
定义对象的迭代语义类似,对象可以自定义一个 [Symbol.unscopables]
字段,在这个字段里出现的方法名,都不会在 with
执行环境下被直接读取。TC39 把 ES6 以后新加的各个原型链方法,包括 copyWithin
, entries
, fill
, find
, findIndex
, flat
, flatMap
, includes
, keys
, values
等等全都加到了 Array.prototype[Symbol.unscopables]
中,以防未来再出现类似问题。
结论:Symbol.unscopables
的引入,唯一作用就是解决 with
执行环境下的历史兼容性问题。它对于新的代码基本是没有意义的。我们甚至可以认为,这是一个不得不暴露的语言实现细节。