为什么扩展本机对象是一种不好的做法?


136

每个JS意见领袖都说,扩展本机对象是一种不良做法。但为什么?我们会获得出色的表现吗?他们是否担心有人会以“错误的方式”这样做,并向中添加了可枚举的类型Object,实际上破坏了任何对象上的所有循环?

TJ Holowaychukshould.js为例。他向中添加了一个简单的吸气剂Object并且一切正常(源代码)。

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(Object(this).valueOf());
  },
  configurable: true
});

这真的很有意义。例如,可以扩展Array

Array.defineProperty(Array.prototype, "remove", {
  set: function(){},
  get: function(){
    return removeArrayElement.bind(this);
  }
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);

是否有反对扩展本机类型的论点?


9
以后,将本机对象更改为包含具有与您自己不同的语义的“删除”功能时,您期望发生什么?您无法控制标准。
ta.speot.12年

1
这不是您的本机类型。这是每个人的本机类型。

6
“他们是否担心有人会以“错误的方式”这样做,并向对象添加了可枚举的类型,实际上破坏了任何对象上的所有循环?” :是的。早在形成这种观点时,就不可能创建不可枚举的属性。现在在这方面可能有所不同,但可以想象每个库都只是按其期望扩展本机对象。我们开始使用名称空间是有原因的。
Felix Kling 2012年

5
对于某些“意见领袖”来说,这是值得的,例如Br​​endan Eich认为扩展本机原型是非常好的。
本杰明·格伦鲍姆

2
富不应该是全球性的,这些天,我们已经包括requirejs,CommonJS的等等
杰米·佩特

Answers:


126

扩展对象时,将更改其行为。

更改仅由您自己的代码使用的对象的行为就可以了。但是,当您更改其他代码也使用的某些行为时,就有可能破坏该其他代码。

当将方法添加到javascript中的对象和数组类中时,由于javascript的工作原理,发生破坏的风险非常高。多年的经验告诉我,这类东西会导致javascript中各种可怕的错误。

如果需要自定义行为,最好定义自己的类(也许是子类),而不是更改本机类。这样,您将不会破坏任何东西。

更改类的工作方式而不对其进行子类化的能力是任何好的编程语言的一项重要功能,但这是很少使用且必须谨慎使用的语言。


2
那么添加类似的东西.stgringify()会被认为是安全的吗?
buschtoens 2012年

7
有很多问题,例如,如果其他一些代码也尝试添加stringify()具有不同行为的自己的方法,会发生什么情况?实际上,这不是日常编程中应该做的事情……如果您要做的只是在这里和那里保存一些代码字符,那就不是。最好定义自己的类或函数以接受任何输入并将其字符串化。大多数浏览器都定义JSON.stringify(),最好是检查它是否存在,如果自己不定义它。
Abhi Beckert 2012年

5
我喜欢someError.stringify()errors.stringify(someError)。它简单明了,非常适合js的概念。我正在做专门绑定到某个ErrorObject的操作。
buschtoens

1
剩下的唯一(但很好)的论据是,一些庞大的宏库可能开始接管您的类型,并且可能会相互干扰。还是还有什么?
buschtoens

4
如果问题是可能发生碰撞,那么是否可以在每次执行此操作时都使用一种模式,在定义该函数之前始终确认该函数未定义?而且,如果始终将第三方库放在自己的代码之前,则应该能够始终检测到此类冲突。
Dave Cousineau

32

没有任何可衡量的缺点,例如性能下降。至少没有人提及。因此,这是个人喜好和经验的问题。

赞成的主要论点:看起来更好,更直观:语法糖。它是特定于类型/实例的功能,因此应专门绑定到该类型/实例。

主要的相反论点:代码可能会干扰。如果库A添加了一个函数,则它可能会覆盖库B的函数。这很容易破坏代码。

两者都有一点。当您依赖两个直接更改您的类型的库时,由于预期的功能可能不相同,最终很有可能会导致代码损坏。我完全同意。宏库不得操纵本机类型。否则,作为开发人员,您将永远不知道幕后的真实情况。

这就是我不喜欢jQuery,下划线等库的原因。它们绝对是经过精心编程的,并且像魅力一样工作,但是它们很大。您仅使用其中的10%,并了解大约1%。

这就是为什么我更喜欢原子方法,因为您只需要真正需要的东西。这样,您总是知道会发生什么。微库只做您想让他们做的事,因此它们不会干扰。在让最终用户知道添加了哪些功能的情况下,扩展本机类型可以被认为是安全的。

TL; DR如有疑问,请不要扩展本机类型。仅当您百分百确定最终用户将了解并希望该行为时,才扩展本机类型。在任何情况下都不要操纵本机类型的现有功能,因为它将破坏现有接口。

如果决定扩展类型,请使用Object.defineProperty(obj, prop, desc); 如果不能,请使用类型的prototype


我最初提出这个问题是因为我希望Errors可通过JSON发送。因此,我需要一种对它们进行分类的方法。error.stringify()感觉比更好errorlib.stringify(error); 正如第二种结构所暗示的那样,我是errorliberror自己而不是自己身上做手术。


我对此有更多意见。
buschtoens

4
您是否暗示jQuery和下划线扩展了本机对象?他们没有。因此,如果您出于这个原因避免使用它们,那就错了。
JLRishe

1
我认为在使用lib A扩展本机对象可能与lib B冲突的论点中遗漏了一个问题:为什么您会有两个库在本质上是如此相似或如此广泛以至于可能发生这种冲突?即,我要么选择lodash要么下划线,但不能同时选择两者。我们当前在javascript中的libraray风景太过饱和,开发人员(通常)在添加它们时变得如此粗心,以至于我们最终避免了最佳实践来安抚我们的Lib Lords
micahblu 2015年

2
@micahblu-库可能只是为了方便内部编程而决定修改标准对象(这通常就是人们想要这样做的原因)。因此,仅因为您不会使用两个具有相同功能的库就不能避免这种冲突。
jfriend00

1
您也可以(从es2015起)创建一个扩展现有类的新类,并在您自己的代码中使用它。因此,如果MyError extends Error它可以有一个stringify不与其他子类冲突的方法。您仍然必须处理不是由您自己的代码生成的错误,因此Error与其他人相比,它可能没有太大用处。
ShadSterling

16

如果您逐案看待,也许某些实现是可以接受的。

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.

覆盖已经创建的方法会带来更多无法解决的问题,这就是为什么在许多编程语言中都普遍采用这种方法来避免这种做法。开发人员如何知道功能已更改?

String.prototype.capitalize = function capitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.

在这种情况下,我们不会覆盖任何已知的核心JS方法,但会扩展String。这篇文章中的一个论点提到新开发者如何知道该方法是否是核心JS的一部分,或者在哪里可以找到文档?如果核心JS String对象获得名为capitalize的方法会怎样?

如果您使用所有开发人员都可以理解的公司/应用程序特定修饰符,而不是添加可能与其他库冲突的名称怎么办?

String.prototype.weCapitalize = function weCapitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.

var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.

如果您继续扩展其他对象,则所有开发人员都会在我们的情况下(在本例中)we遇到它们,这将通知他们这是公司/应用程序特定的扩展。

这不会消除名称冲突,但会减少可能性。如果您确定扩展核心JS对象适合您和/或您的团队,那么也许适合您。


15

我认为这是一种不良做法。主要原因是集成。引用should.js文件:

OMG IT会扩展对象???!?!@是的,是的,只有一个吸气剂就可以了,不,它不会破坏您的代码

那么,作者怎么知道?如果我的模拟框架也一样怎么办?如果我的诺言lib也一样怎么办?

如果您是在自己的项目中执行此操作,那很好。但是对于图书馆来说,这是一个糟糕的设计。Underscore.js是正确执行操作的示例:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()

2
我确定TJ的回应是不要使用这些承诺或嘲笑框架:x
Jim Schubert

_(arr).flatten()示例实际上说服了我不要扩展本机对象。我个人这样做的原因纯粹是语法上的。但这满足了我的美学感觉:)即使使用一些更常规的函数名称(如foo(native).coolStuff()将其转换为某些“扩展”对象)在语法上也看起来不错。因此,谢谢!
egst

7

扩展内置原型确实是一个坏主意。但是,ES2015引入了一种新技术,可以用来获得所需的行为:

利用WeakMaps将类型与内置原型相关联

以下实现扩展了NumberArray原型,而完全不涉及它们:

// new types

const AddMonoid = {
  empty: () => 0,
  concat: (x, y) => x + y,
};

const ArrayMonoid = {
  empty: () => [],
  concat: (acc, x) => acc.concat(x),
};

const ArrayFold = {
  reduce: xs => xs.reduce(
   type(xs[0]).monoid.concat,
   type(xs[0]).monoid.empty()
)};


// the WeakMap that associates types to prototpyes

types = new WeakMap();

types.set(Number.prototype, {
  monoid: AddMonoid
});

types.set(Array.prototype, {
  monoid: ArrayMonoid,
  fold: ArrayFold
});


// auxiliary helpers to apply functions of the extended prototypes

const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);


// mock data

xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];


// and run

console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);

如您所见,即使原始原型也可以通过此技术进行扩展。它使用映射结构和Object标识将类型与内置原型相关联。

我的示例启用了一个reduce仅将a Array作为其单个参数的函数,因为它可以从Array本身的元素中提取信息,以了解如何创建一个空的累加器以及如何使用此累加器连接元素。

请注意,我本可以使用普通Map类型的,因为弱引用仅代表内置的原型(永远不会进行垃圾收集)时没有意义。但是,WeakMap除非您拥有正确的密钥,否则a 是不可迭代的并且无法检查。这是一个理想的功能,因为我想避免任何形式的类型反射。


这是WeakMap的不错用法。小心xs[0]在空数组上。type(undefined).monoid ...
感谢您

@naomik我知道-请参阅我的最新问题第二个代码段。

支持此@ftor的标准如何?
onassar

4
-1; 这有什么意义?您只是创建一个单独的全局字典将类型映射到方法,然后按类型在该字典中显式查找方法。结果,您失去了对继承的支持(Object.prototype无法在上调用以这种方式“添加”到的方法Array),并且与真正扩展原型相比,该语法明显更长或更难看。仅使用静态方法创建实用工具类几乎总是比较简单。这种方法的唯一优点是对多态性的支持有限。
Mark Amery

3
@MarkAmery嘿朋友,你不明白。我不会失去继承,但要摆脱它。继承是如此80年代,您应该忘记它。破解的全部目的是模仿类型类,简而言之就是重载函数。但是,有一个合理的批评:模仿Java语言中的类型类是否有用?不,不是。而是使用本地支持它们的类型化语言。我很确定这就是您的意思。

7

为什么扩展本机对象的另一个原因:

我们使用Magento,它使用prototype.js并在本机Objects上扩展了很多内容。在您决定使用新功能之前,这很正常,这才是大麻烦开始的地方。

我们在其中一页上引入了Webcomponents,因此webcomponents-lite.js决定替换IE中的整个(本机)事件对象(为什么?)。当然,这会破坏prototype.js,进而会破坏Magento。(直到发现问题,您可能需要花费大量时间来追查问题)

如果您喜欢麻烦,请继续这样做!


4

我可以看到三个不这样做的原因(至少在应用程序内部),其中两个在现有答案中得到了解决:

  1. 如果做错了,您会不小心将一个可枚举的属性添加到扩展类型的所有对象中。使用可以轻松解决Object.defineProperty,默认情况下会创建不可枚举的属性。
  2. 您可能会与正在使用的库产生冲突。勤奋可以避免;只需在添加一些东西到原型之前检查您使用的库定义了哪些方法,检查升级时的发行说明并测试您的应用程序。
  3. 您可能会与本​​机JavaScript环境的将来版本产生冲突。

第三点可以说是最重要的一点。通过测试,您可以确保原型扩展不会与您使用的库引起任何冲突,因为您可以决定要使用的库。假设您的代码在浏览器中运行,则本机对象不是这样。如果您定义Array.prototype.swizzle(foo, bar)今天,明天又将Google添加Array.prototype.swizzle(bar, foo)到Chrome,您最终可能会遇到一些困惑的同事,他们想知道为什么.swizzle的行为似乎与MDN上记录的不匹配。

(另请参见有关mootools如何摆弄他们不拥有的原型故事,这迫使ES6方法重命名以避免网络中断。)

通过为添加到本机对象的方法使用特定于应用程序的前缀可以避免这种情况(例如,使用define Array.prototype.myappSwizzle代替Array.prototype.swizzle),但这有点丑陋;通过使用独立的实用程序函数而不是扩展原型,它同样可以解决。


2

性能也是一个原因。有时您可能需要遍历键。做这件事有很多种方法

for (let key in object) { ... }
for (let key in object) { if (object.hasOwnProperty(key) { ... } }
for (let key of Object.keys(object)) { ... }

我通常使用for of Object.keys()它做正确的事,并且相对简洁,无需添加支票。

但是,它要慢得多

优胜劣汰结果

只是猜测Object.keys慢的原因是显而易见的,Object.keys()必须进行分配。实际上AFAIK从那以后必须分配所有密钥的副本。

  const before = Object.keys(object);
  object.newProp = true;
  const after = Object.keys(object);

  before.join('') !== after.join('')

JS引擎可能会使用某种不可变键结构,以便Object.keys(object)返回对迭代不可变键进行迭代的引用,并object.newProp创建一个全新的不可变键对象,但是无论如何,它的速度显然要慢15倍

即使检查hasOwnProperty速度也要慢2倍。

所有这些的要点是,如果您具有对性能敏感的代码并且需要遍历键,那么您希望能够使用for in而不必调用hasOwnProperty。如果您尚未修改,则只能这样做Object.prototype

请注意,如果您使用Object.defineProperty添加的东西无法枚举来修改原型,那么在上述情况下它们不会影响JavaScript的行为。不幸的是,至少在Chrome 83中,它们确实会影响性能。

在此处输入图片说明

我添加了3000个非枚举属性,以试图强制出现任何性能问题。仅使用30个属性,测试太接近以至于无法确定是否有任何性能影响。

https://jsperf.com/does-adding-non-enumerable-properties-affect-perf

Firefox 77和Safari 13.1在增强和未增强类之间的性能没有差异,也许v8在该区域得到修复,您可以忽略性能问题。

但是,让我也添加一个故事Array.prototype.smoosh。简短的版本是Mootools,这是一个受欢迎的库,它们是自己制作的Array.prototype.flatten。当标准委员会试图添加本地人时,Array.prototype.flatten他们发现不能破坏很多站点是不可能的。发现中断的开发人员建议将es5方法命名smoosh为一个玩笑,但人们吓坏了,不明白这是个玩笑。他们定居flat而不是flatten

这个故事的寓意是您不应该扩展本机对象。如果这样做,您可能会遇到同样的东西中断问题,除非您的特定库恰好与MooTools一样流行,否则浏览器供应商不太可能解决您引起的问题。如果您的图书馆确实受到欢迎,那将是迫使其他所有人解决您引起的问题的一种方式。所以,请不要扩展本机对象


等待,hasOwnProperty告诉您属性是在对象中还是在原型中。但是for in循环只会为您带来无数的属性。我刚刚尝试了一下:向Array原型添加一个不可枚举的属性,它不会显示在循环中。无需没有修改的原型简单的循环工作。
ygoe

但是由于其他原因,这仍然是一种不好的做法;)
gman
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.