是否可以修改 JavaScript 的本地对象(native object)

本文翻译自RapydScript的作者 Alexander Tsepkov 的博客。
原文地址:http://blog.pyjeon.com/2014/05/17/modifying-native-javascript-objects/

注:本地对象 (native object) 是指“独立于宿主环境的 ECMAScript 实现提供的对象”,比如 Global 和 Math等。

在JavaScript社区,对于JavaScript本地对象(Array, Number, String, Object)的修改一直存在着争论。一些开发者认为这不可接受,而另一些开发者却非常赞成。如果你阅读过 RapydScript(支持使用类似Python语法编写JavaScript代码的预编译器)的 stdlib 的代码,那么你可能已经知晓我的立场,我实际是支持在需要时做适当的修改。在JavaScript方面,我也许没有那些大师那么多的经验,但是在判断编程语言的设计实践好或不好方面,我还是有足够经验的。

大部分争论其实非常无力,因为他们总是拿开发者重写本地对象本身有的方法来说事,但其实毫无疑问这是不好的事情。试想一下,如果你在并不知情,然而调用时也仍按原来的实现传递中相同的参数,那么执行的结果是不一样的。例如,我决定对 String.prototype.replace 进行重写,使其可以像其他语言一样,替换掉所有出现的位置,而不只是第一次出现的时候:

String.prototype.replace = function(orig, sub) {
    return this.split(orig).join(sub);
}

这样的话,页面中其它代码本来认为 replace 只会替换掉第一个元素的库或组件的逻辑会遭到破坏,并导致这些代码再也不能按原有的方式工作。因此,我完全不赞成重写已经存在的方法来做更多的事情。所以,保持JavaScript语言的基本子集按所有人都期望的方式工作是很重要的事。例如,如果你不能保证你的你的房子基础是平稳的,那么你也不能保证它不会倒塌。

但还是有一些情况下,我们可以只将已有方法的功能进行扩展,使其可以在原有参数下按照我们原来期望的方式工作的同时,还可以在传递另外参数的时候做些别的事情:

Array.prototype.pop = function(index) {
    if (!arguments.length) {
        index = this.length - 1;
    }
    return this.splice(index, 1)[0];
}

现在, myArray.pop() 仍然可以按过去的方式工作,但是如果调用 myArray.pop(0) 的话,函数的行为将与myArray.shift()或者调用时传递中间位置索引的splice()函数作用相同。实际上,像这样重写已有函数的做法的唯一不好之处,是我们使得函数的运行较原来的函数要慢(本地函数运行会快一点)。但认为这种对 pop() 的改写根本不好的人的理由是它会打破另外一个逻辑。那就是过去认为对 pop() 带一个参数的调用方式是错误的(虽然原来的函数只是简单地忽略掉参数),或者说带参数的调用应认为是不合法的。但实际上,函数中的bug在于糟糕的调用方式,而不是重写函数时的逻辑。这也是我喜欢Python的地方,它会在参数不符合要求时立即报告,而不是忽略他们以致之后出现更大的问题。

从另一个方面来说,向本地对象新添加新的方法是比较良好的方式。这里我们实现如下功能时的例子:

Array.prototype.copy = function() {
     return this.slice();
 }

但是在这样做的时候,还是应该要意识到是否有其他库也添加了同名但不同功能的方法。如果有的话,你就需要用一个不同的名字。如果你对本地对象使用的库或API集不可能多于一个,那么这个问题并不存在。到目前为止,我所知道的关于对修改本地对象的问题最主要的反对意见,是JavaScript存在未来添加同名方法并造成命名冲突的潜在可能。

但你可能会注意到,如果你试图向本地对象 object 追加方法的话,那么页面中的所有与jQuery相关的代码都不能正常工作。这个问题并不是由本地方法的重写引起。而是jQuery开发者编写了错误假设下的bug代码。jQuery的开发者就是属于那些认为向JavaScript本地对象添加方法是糟糕行为的人,因此,在使用 for key in Object 方式扫描对象属性时,错误地认为没有人向 object 追加了内容,而不是正确地检测其是否属于对象自有的属性。如果你要避免造成与jQuery同样的错误,那么应保证迭代只在对象自有关键字中进行:

for (var key in obj) {
    if obj.hasOwnProperty(key) {
        ...
    }
}

另外,如果你只关心使用最新的浏览器的话,那么你还可以考虑使用 Object.getOwnPropertyNames() 来解决这个问题。

在JavaScript中来说,默认对对象的每个属性进行迭代是非常糟糕的设计,但是现在已经不能被改变了。jQuery的开发者之前已经出来说明了这个问题,但是他们声称这是出于性能的考虑。独立测试表明,添加检查代码对性能只有5%的影响,因此我不同意这种影响性能的说法。因为,jQuery早已经为了好用的名字而牺牲了太多的性能(例如, show/hide 逻辑为了正确得到对象的当前状态做了安全性检查)。在我看来,John Resig(jQuery之父)在此问题的立场与没有做除零检查(JavaScript中会自动的返回infinity)是相同的,先是声称这是出于性能原因,然后又认为任何向除法传递参数0的做法是错误的。

我没有检查jQuery 2.0中是否修正了这个问题。但是对老的浏览器来说,不使用 Object.getOwnPropertyNames() 是没有理由的。

因此,在使用jQuery的同时,不要在任何地方尝试对 Object 进行修改(其他的本地对象没有问题;当然,如果你没有使用jQuery,就更不是问题了)。另外,值得一提的是,如果你不打算支持旧的浏览器(比如 IE8),使用 defineProperty 而不是向prototype添加方法将是更好的方式。因为,在对对象做迭代时,这种方式添加的方法都会被忽略掉。那么,jQuery的bug也不会存在了。

就我的立场来说,我支持向本地对象添加功能,但不要删除本地对象上的东西。另外,将已有方法的工作方式修改得与过去不同,但却使用相同的方法签名,这也算删除了方法,同样也是非常不好的方式。我在RapydScript的 stdlib2 库的代码中移除了与重写本地对象方法相关的依赖。这么做的主要原因是为了与jQuery一致,因此对标准库代码进行了适当清理。但是,这并不表明我反对修改本地对象的代码。

更多地了解RapydScript,请访问 RapydScript.cn 。

你可能感兴趣的:(Rapyd,javascript,native,对象,jquery,A_前端技术)