有时,一个提议的特性(方法、全局变量等)的名称与现有的代码相冲突,必须加以改变。这篇博文解释了这种情况如何发生,并列出了被重新命名的特性。
不断发展的JavaScript。不要破坏网络!
进化JavaScript的一个核心原则是不要 "破坏网络"。所有现有的代码必须在语言中加入新功能后继续工作。
这样做的坏处是,现有的怪癖不能从语言中删除。但好处是相当大的。旧的代码可以继续工作,升级到新的ECMAScript版本很简单,等等。
关于这个话题的更多信息,请参见《不断发展的JavaScript》一节。不要破坏网络 "中的 "不耐烦的程序员的JavaScript"。
当为一个新的特性(如方法名称)选择了一个名称时,一个重要的测试是在一个 夜间发布(早期的预发布版本)的浏览器中添加该功能,并检查是否有网站出现了错误。接下来的章节涵盖了四个冲突的来源,在这些冲突中,过去的情况是这样的,功能必须被重新命名。
冲突的来源:向内置原型添加方法
在JavaScript中,我们可以通过改变内置值的原型来为其添加方法。
以这种方式改变语言是很迷人的。这种运行时的修改被称为猴子补丁。下一小节将解释这个术语。然后我们将看看这种修改的弊端。
术语:猴子补丁
如果我们给内置原型添加方法,我们就是在运行时修改软件系统。这样的修改被称为猴子补丁。我尽量避免使用专业术语,包括这个术语,但意识到这一点是好的。对其含义有两种可能的解释(引用维基百科)。
-
它来自 "一个更早的术语,游击队补丁,指的是在运行时偷偷摸摸地改变代码--而且可能与其他类似的补丁不兼容。guerrilla这个词与gorilla同音(或几乎同音),后来变成了monkey,可能是为了让补丁听起来不那么吓人。"
-
它 "指的是对代码的'胡闹'(捣乱)"。
反对改变内置原型的理由
对于任何类型的全局命名空间,总是存在名称冲突的风险。如果有解决冲突的机制,这种风险就会消失--例如。
-
全局模块是通过裸模块指定器或URL来识别的。前者之间的名称冲突通过npm注册表来防止。后者之间的名称冲突是通过域名注册处来防止的。
-
符号被添加到JavaScript中以避免方法之间的名称冲突。例如,任何对象都可以通过添加一个键为
Symbol.iterator的方法成为可迭代的。由于每个符号都是唯一的,这个键永远不会与任何其他的属性键发生冲突。
然而,带有字符串键的方法会导致名称冲突。
- 不同的库可能对它们添加到
Array.prototype的方法使用相同的名字。 - 如果一个名字已经被某个库在任何地方使用了,那么它就不能再用于JavaScript标准库的一个新特性。有几种情况下,这是个问题。它们将在下一节中描述。
具有讽刺意味的是,小心翼翼地添加一个方法会使事情变得更加糟糕--比如说。
这里,我们检查一个方法是否已经存在。如果没有,我们就添加它。
如果我们在实现一个 多重填充为不支持它的引擎添加一个新的JavaScript方法。(顺便说一下,这是修改内置原型的一个合法用例。也许是唯一的一个)。
然而,如果我们对一个普通的库方法使用这种技术,而JavaScript后来又得到了一个同名的方法,那么这两种实现的工作方式就不一样了,所有使用库方法的代码在使用内置方法时都会中断。
不得不改变名称的拟议原型方法的例子
-
ES6的方法
String.prototype.includes()原本是.contains(),这与JavaScript框架MooTools全局添加的一个方法相冲突(bug报告)。 -
ES2016的方法
Array.prototype.includes()原本是.contains(),与MooTools添加的一个方法发生冲突(bug报告)。 -
ES2019的方法
Array.prototype.flat()原本是.flatten(),与MooTools冲突了(bug报告,博客文章)。
修改內建的原型並不總是被認為是不好的風格
你可能想知道。MooTools的創造者怎麼會這麼不小心?然而,為內建型態原型增加方法並不總是被認為是壞的風格。在ES3(1999年12月)與ES5(2009年12月)之間,JavaScript是一種停滯不前的語言。MooTools和Prototype等框架改善了它。他们的方法的弊端只有在JavaScript的标准库再次增长后才变得明显。
冲突的来源:检查一个属性的存在
ES2022的方法Array.prototype.at() ,最初是.item() 。它不得不被重新命名,因为以下库检查属性.item ,以确定一个对象是否是一个HTML集合(而不是一个数组)。Magic360、YUI 2、YUI 3(提案中的相关部分)。
冲突的来源:检查全局变量的存在
从ES2020开始,我们可以通过以下方式访问全局对象 globalThis.Node.js一直使用名称global 来实现这一目的。最初的计划是为所有平台统一这个名字。
然而,下面这个模式经常被用来确定当前的平台。
如果浏览器也有一个名为global 的全局变量,这个模式(以及类似的模式)就会失效。因此,标准化的名称被改为globalThis 。
冲突的来源:通过with 创建局部变量
JavaScript的with 语句
使用JavaScript的with 语句已经被劝阻了很久,甚至被定为非法,在 严格模式,这是在ECMAScript 5中引入的。在其他地方,严格模式在ECMAScript模块中是活跃的。
with 语句将一个对象的属性变成了局部变量。
由于with ,出现了冲突
框架Ext.js使用的代码与下面的片段松散地相似。
当ES6方法Array.prototype.values() 被添加到JavaScript中时,如果用一个数组(B行)来调用它,就会破坏myFunc() 。with 语句将数组values 的所有属性变成了局部变量。其中一个是继承的属性.values 。因此,A行的语句记录了Array.prototype.values ,而不是参数values 了(错误报告1,错误报告2)。
不可操作性:防止由with 引起的冲突
公共符号Symbol.unscopables让一个对象从with 语句中隐藏一些属性。它在标准库中只使用了一次,用于 Array.prototype:
assert.deepEqual(
Array.prototype[Symbol.unscopables],
{
__proto__: null,
at: true,
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
flat: true,
flatMap: true,
includes: true,
keys: true,
values: true,
}
);
不可复制的列表包括values 和与它同时或之后引入的方法。
结论
我们已经看到了拟议的JavaScript结构与现有代码发生名称冲突的四种方式。
- 为内置原型添加方法
- 检查一个属性的存在
- 检查全局变量的存在
- 创建局部变量
with
有些冲突的来源很难预测,但有几条一般的规则存在。
- 不要改变全局数据。
- 避免检查全局数据的存在或不存在。
- 注意内置值在未来可能会得到额外的属性(自己的或继承的)。
对于一个库来说,为JavaScript值提供功能的最安全的方式是通过函数。如果JavaScript有一个管道操作符,我们甚至可以像方法一样使用它们。