【问题标题】:Extending JavaScript's built-in types - is it evil? [closed]扩展 JavaScript 的内置类型——它是邪恶的吗? [关闭]
【发布时间】:2017-03-13 13:50:45
【问题描述】:

我读过一些文章,其中建议在 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;
}());

所以现在,我可以将一个数组传递给一个新的 Enumerable,然后首先调用 Enumerable 实例。伟大的!我尊重 ECMAScript-20xx 规范,我仍然可以做我想做的事。

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

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

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

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

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

【问题讨论】:

标签: javascript


【解决方案1】:

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

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

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

new google.maps.Map(...)

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

【讨论】:

    【解决方案2】:

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

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

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

    问题是,spec authors are increasingly wary of not destroying the existing web,所以如果太多现有网站中断,他们必须重命名未来的功能。 (这也是为什么你不应该为尚未最终确定的 Web 平台规范创建 polyfill,顺便说一句)

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

    如果您在函数或块范围内使用变量,并且不依赖未来的功能,那么应该没问题。

    函数范围示例:

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

    块范围示例:

    {
        const Enumerable = {};
    }
    

    【讨论】:

      【解决方案3】:

      警告:自以为是的答案。

      我不认为扩展 JavaScript 对象是“邪恶的”。显然存在危险,您需要意识到这些危险(正如您所清楚的那样)。我对该声明的唯一警告是,如果你这样做了,你必须引入一种提醒自己注意冲突的方法。

      换句话说,不是简单地在数组原型上定义一个first函数,你应该首先看看Array.prototype.first是否被定义,如果是,然后抛出(或以其他方式提醒自己这个冲突) .只有在未定义的情况下,您才应该允许您的代码定义它,否则您将替换每个人的定义。

      【讨论】:

      • 该工具(扩展 Javascript 对象)在道德上是中立的。感谢您指出这一点。
      【解决方案4】:

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

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

      我认为主要原因是ECMA6中有一个非常好的替代方案,即子类化。不要使用任何原型,只需使用这个:

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

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

      【讨论】:

      • JavaScript 一直是一种 OO 语言,继承一直是可能的。 ES6 类只是在它之上的更好的语法。
      • 对...所以否决票是因为替代方案无关紧要?扩展是我回答的重点。每个答案都是关于原型设计,就像问题一样,我的回答是他们构建了很好的语法,这样您就可以扩展内置类型而不会破坏它。
      • 这个myArray 类与 OP 的任何示例有何相同之处? OP 的第一个代码示例的优点是您可以执行[1, 2, 3].first(),这显然是您的代码无法实现的。此外,您的回答并未回答实际问题。
      • @idmean 对,你必须做 var myArray = new myArray(1,2,3)。但之后你可以执行 myArray.first、myArray.push(4) 等。除了初始初始化之外,它的作用与 Array 完全相同。其中,顺便说一句,除了使用原型之外,OP 所做的就是创建自己的 var 'Enumeration'。我的意思是,如果您阅读了我的回答,那么标准上接受的原因有两个,即为什么不这样做,即浏览器支持和 for...in 支持。但我的主要原因是“如果有更好的方法,那就用更好的方法,避免任何潜在的陷阱”。
      • 澄清一下:我的问题是,您将其呈现为好像扩展类是 ES6 独有的新事物,而事实上这一直是可能的,并且实际上仍在使用原型兜帽。所有 ES6 类所做的只是对语法进行粉饰,基本机制没有任何改变。而且您仍然容易与那些扩展类发生名称冲突。
      【解决方案5】:

      这里的危险比与未来的 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 定义,最后一个是wins。想象一下,当您的单元测试通过,您的合作伙伴团队的单元测试通过时,您将经历一年的痛苦,但是您不能将代码库合并到一个网页中而不会发生真正奇怪的崩溃,具体取决于加载的库最后的。这是可维护性噩梦

      然后你和你的搭档团队进入一个房间,争论六个小时关于谁对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); }
      

      【讨论】:

        【解决方案6】:

        除了提供的所有令人惊叹的答案之外,您还可以像 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,您可以使用浏览器拥有的Enumerable,如果没有,您可以使用自己的。
        这非常依赖于您的代码库。

        【讨论】:

        • 但是,没有标准的first方法,所以你不能polyfill它,只有当它不存在时才覆盖它的方法如果它不起作用后来确实会标准化为不同的东西。
        • @Bergi 这是一个缺点:/ 但是使用一个半体面的工具包(浏览器控制台),您可以尝试追溯所有正在发生的错误。根据代码的大小,找到它的难度适中。
        猜你喜欢
        • 2010-11-26
        • 2010-12-23
        • 2011-03-21
        • 1970-01-01
        • 1970-01-01
        • 2011-04-18
        • 2014-03-28
        • 1970-01-01
        • 2012-01-23
        相关资源
        最近更新 更多