扩展JavaScript的内置类型 - 它是邪恶的吗?

Extending JavaScript's built-in types - is it evil?

本文关键字:邪恶 JavaScript 内置 置类型 扩展      更新时间:2023-09-26

我读过几篇文章,建议在JavaScript中扩展内置对象是一个坏主意。例如,假设我添加一个first函数来Array...

Array.prototype.first = function(fn) {
    return this.filter(fn)[0];
};

太好了,所以现在我可以基于谓词获得第一个元素。但是,当 ECMAScript-20xx 决定首先添加到规范中,并以不同的方式实现它时,会发生什么?- 好吧,突然之间,我的代码假设了一个非标准的实现,开发人员失去了信心,等等。

因此,我决定创建自己的类型...

var Enumerable = (function () {
    function Enumerable(array) {
        this.array = array;
    }
    Enumerable.prototype.first = function (fn) {
        return this.array.filter(fn)[0];
    };
    return Enumerable;
}());

所以现在,我可以将数组传递到新的枚举中,并首先调用可枚举实例。伟大!我尊重 ECMAScript-20xx 规范,我仍然可以做我想让它做的事情。

然后发布了ES20XX+1规范,引入了一种Enumerable类型,它甚至没有第一种方法。现在怎么办?

本文的关键归结为这一点;扩展内置类型有多糟糕,我们如何避免将来的实现冲突?

注意:使用命名空间可能是解决此问题的一种方法,但话又说回来,事实并非如此!

var Collection = {
    Enumerable: function () { ... }
};

当 ECMAScript 规范引入Collection时会发生什么?

这正是您必须尝试尽可能少地污染全局命名空间的原因。完全避免任何类型的冲突的唯一方法是定义IIFE中的所有内容:

(function () {
    let Collection = ...
})();

当且仅当您需要定义全局对象时,例如,因为您是一个库并且希望被第三方使用,您应该定义一个极不可能冲突的名称,例如因为它是您的公司名称:

new google.maps.Map(...)

任何时候你定义一个全局对象,其中包括现有类型的新方法,你都会冒着其他库或一些未来的 ECMAScript 标准试图在未来某个时候选择这个名称的风险。

第一种方法的问题在于,您扩展了页面上所有脚本的原型。

如果要包含依赖于新的本机 ES-20xx Array.prototype.first 方法的第三方脚本,那么您将使用代码破坏它。

第二个例子只是真正的问题,如果你要使用全局变量(我希望你不要这样做......然后可能会发生同样的问题。

问题是,规范作者越来越担心不破坏现有的网络,所以如果太多现有网站中断,他们必须重命名未来的功能。(这也是为什么您不应该为尚未最终确定的 Web 平台规范创建 polyfill,顺便说一句(

如果您创建一个扩展本机对象的库或框架,并且被 100 或 1000 个网站使用,那么问题肯定会更大。那是你开始阻碍标准过程的时候。

如果你在函数或块作用域中使用变量,并且不依赖于未来的功能,你应该没问题。

函数作用域示例:

(function() {
  var Enumerable = {};
})();

块范围示例:

{
    const Enumerable = {};
}

这里的危险比与未来的ES20xx标准发生冲突要微妙得多。至少在那里你可以删除自己的代码,并处理任何潜在的行为差异的后果。

危险在于,您和其他人都决定使用不一致的行为扩展内置类型。

考虑这样的事情

// In my team's codebase
// Empty strings are empty
String.prototype.isEmpty = function() { return this.length === 0; }
// In my partner team's codebase
// Strings consisting any non-whitespace chars are non-empty
String.prototype.isEmpty = function() { return this.trim().length === 0; }

你有两个同样有效的isEmpty定义,最后一个定义是胜利。想象一下,当你的单元测试通过,你的合作伙伴团队的单元测试通过时,你将在一年后遇到痛苦,但你不能将你的代码库合并到一个网页中,而不会得到非常奇怪的崩溃,这取决于谁的库最后加载。这是可维护性的噩梦

然后,你可以和你的合作伙伴团队进入一个房间,争论六个小时,讨论谁对isEmpty的定义是"正确的",将做出一个武断的决定,你们两个中的一个可以查看代码库中isEmpty的每一个实例,以确定如何使其与新功能一起使用。您可能会在此过程中引入一些新的错误。

然后再次重复整个过程,当你发现你们都想要一个关于数字的toInteger函数,但没有想到召开全公司范围的会议来处理负数时。

// -3.1 --> -4    
Number.prototype.toInteger = function() { return Math.floor(this); }
// -3.1 --> -3    
Number.prototype.toInteger = function() { return Math.trunc(this); }

警告:固执己见的答案。

我不认为扩展JavaScript对象是"邪恶的"。显然存在危险,您需要意识到这些危险(正如您显然一样(。我对这句话的唯一警告是,如果你这样做,你必须引入一种提醒自己注意冲突的方法。

换句话说,与其简单地在数组原型上定义一个first函数,不如首先查看是否定义了Array.prototype.first,如果定义了,然后抛出(或以其他方式提醒自己注意此冲突(。仅当它未定义时,才应允许代码定义它,否则将替换每个人的定义。

作为所有令人惊讶的答案的替代方案,您可以执行与 polyfill 相同的操作。

通常,当有人编写与原型混淆的东西时,会检查是否已经定义了值。

下面是一个示例:

if(!Array.prototype.first) {
    Array.prototype.first = function(fn) {
        return this.filter(fn)[0];
    };
}

这对于小段代码很好,但可能会在大型库上带来问题。
实现匿名自调用函数可以帮助解决这个问题,如下所示:

(function(){
    // Leave if it is already defined
    if(Array.prototype.first) {
        return;
    }
    Array.prototype.first = function(fn) {
        return this.filter(fn)[0];
    };
})();

这比之前提供的所有解决方案都有好处。

如果要减少下载代码的占用空间,可以动态加载所需的脚本。
如果创建了新Enumerable,您可以使用浏览器拥有的浏览器,如果没有,则可以使用您自己的浏览器。
这完全取决于您的代码库。

这个问题和这些答案似乎非常面向ECMA5,而不是6。当 ECMA6 带来类时,原型的使用是毫无用处的。没有太多重要的理由不添加到内置的原型中,但大多数情况下,更多的是"如果你能避免它,你应该"的情况。

首先,在添加到内置类型时,您可能会破坏较旧的浏览器,也可能破坏"for(i in array("格式,因为如何...在基本级别调用,它可能会产生意外的结果。

在我看来,主要原因是 ECMA6 中有一个非常好的替代方案,那就是子类化。无需使用任何原型,只需使用以下命令:

class myArray extends array {
    constructor() { ... }
    getFirst() {
        return this[0];
    }
    // Or make a getter
    get first() { 
        return this[0]; 
    }
}

您可以将所需的任何函数放入getter中,但它仍然是独立的代码,不会干扰未来的命名空间。但是,因为您是子类化,所以您不必重写数组。所以,与其说它是纯粹的邪恶,不如说编码更好不要这样做,就像在一个长页面中编写所有 JavaScript 不一定是邪恶的一样,但如果你能避免它,这是更好的标准做法。