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

时间:2016-10-31 13:09:40

标签: javascript

我已经阅读了一些文章,建议在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时会发生什么?

6 个答案:

答案 0 :(得分:14)

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

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

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

new google.maps.Map(...)

每次定义一个全局对象(包括现有类型的新方法)时,您都会冒一些其他库或某些未来ECMAScript标准的风险,试图在将来某个时候共同选择该名称。

答案 1 :(得分:6)

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

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

第二个例子只是一个问题,如果你要使用全局变量(我希望你没有这样做......)。然后可能会出现同样的问题。

问题是spec authors are increasingly wary of not destroying the existing web,因此如果现有网站太多,他们必须重命名未来的功能。 (这也是为什么你不应该为尚未最终确定的网络平台规范创建polyfill的原因,BTW)

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

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

功能范围示例:

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

阻止范围示例:

{
    const Enumerable = {};
}

答案 2 :(得分:3)

这里的危险比未来的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); }

答案 3 :(得分:2)

警告:有意见的回答。

我不相信扩展JavaScript对象是“邪恶的”。显然存在危险,你需要了解这些(如你所知)。我对该陈述的唯一警告是,如果你这样做,你必须引入一种警告冲突的方法。

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

答案 4 :(得分:1)

作为所有呈现的惊人答案的替代方案,您可以像polyfills那样做。

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

以下是一个例子:

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,您可以使用浏览器拥有的E,如果没有,则可以使用您自己的2D。 这极其依赖于您的代码库。

答案 5 :(得分:-1)

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

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

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

class myArray extends array {
    constructor() { ... }

    getFirst() {
        return this[0];
    }

    // Or make a getter

    get first() { 
        return this[0]; 
    }
}

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