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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14034180/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 15:30:32  来源:igfitidea点击:

Why is extending native objects a bad practice?

javascriptprototypeprototypal-inheritance

提问by buschtoens

Every JS opinion leader says that extending the native objects is a bad practice. But why? Do we get a perfomance hit? Do they fear that somebody does it "the wrong way", and adds enumerable types to Object, practically destroying all loops on any object?

每个 JS 意见领袖都说扩展原生对象是一种不好的做法。但为什么?我们的性能受到了影响吗?他们是否担心有人“以错误的方式”执行此操作,并将可枚举类型添加到Object,实际上会破坏任何对象上的所有循环?

Take TJ Holowaychuk's should.jsfor example. He adds a simple getterto Objectand everything works fine (source).

TJ Holowaychukshould.js为例。他增加了一个简单的getterObject,一切工作正常(来源)。

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

This really makes sense. For instance one could extend Array.

这确实有道理。例如,可以扩展Array.

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

Are there any arguments against extending native types?

是否有任何反对扩展本机类型的论据?

采纳答案by Abhi Beckert

When you extend an object, you change its behaviour.

当你扩展一个对象时,你改变了它的行为。

Changing the behaviour of an object that will only be used by your own code is fine. But when you change the behaviour of something that is also used by other code there is a risk you will break that other code.

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

When it comes adding methods to the object and array classes in javascript, the risk of breaking something is very high, due to how javascript works. Long years of experience have taught me that this kind of stuff causes all kinds of terrible bugs in javascript.

当在 javascript 中向对象和数组类添加方法时,由于 javascript 的工作方式,破坏某些东西的风险非常高。多年的经验告诉我,这种东西会导致 javascript 中出现各种可怕的错误。

If you need custom behaviour, it is far better to define your own class (perhaps a subclass) instead of changing a native one. That way you will not break anything at all.

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

The ability to change how a class works without subclassing it is an important feature of any good programming language, but it is one that must be used rarely and with caution.

改变一个类的工作方式而不对其进行子类化的能力是任何优秀编程语言的一个重要特性,但它必须很少使用并且必须谨慎使用。

回答by buschtoens

There's no measurable drawback, like a performance hit. At least nobody mentioned any. So this is a question of personal preference and experiences.

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

The main pro argument:It looks better and is more intuitive: syntax sugar. It is a type/instance specific function, so it should be specifically bound to that type/instance.

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

The main contra argument:Code can interfere. If lib A adds a function, it could overwrite lib B's function. This can break code very easily.

主要的反对意见:代码可以干扰。如果lib A 添加了一个函数,它可能会覆盖lib B 的函数。这很容易破坏代码。

Both have a point. When you rely on two libraries that directly change your types, you will most likely end up with broken code as the expected functionality is probably not the same. I totally agree on that. Macro-libraries must not manipulate the native types. Otherwise you as a developer won't ever know what is really going on behind the scenes.

两者都有道理。当您依赖两个直接更改您的类型的库时,您很可能最终会得到损坏的代码,因为预期的功能可能不一样。我完全同意这一点。宏库不得操作本机类型。否则,您作为开发人员将永远不知道幕后到底发生了什么。

And that is the reason I dislike libs like jQuery, underscore, etc. Don't get me wrong; they are absolutely well-programmed and they work like a charm, but they are big. You use only 10% of them, and understand about 1%.

这就是我不喜欢 jQuery、下划线等库的原因。别误会我的意思;它们的程序设计得非常好,它们的工作方式非常迷人,但它们很大。你只使用了其中的 10%,理解了大约 1%。

That's why I prefer an atomistic approach, where you only require what you really need. This way, you always know what happens. The micro-libraries only do what you want them to do, so they won't interfere. In the context of having the end user knowing which features are added, extending native types can be considered safe.

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

TL;DRWhen in doubt, don't extend native types. Only extend a native type if you're 100% sure, that the end user will know about and want that behavior. In nocase manipulate a native type's existing functions, as it would break the existing interface.

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

If you decide to extend the type, use Object.defineProperty(obj, prop, desc); if you can't, use the type's prototype.

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



I originally came up with this question because I wanted Errors to be sendable via JSON. So, I needed a way to stringify them. error.stringify()felt way better than errorlib.stringify(error); as the second construct suggests, I'm operating on errorliband not on erroritself.

我最初提出这个问题是因为我希望Errors 可以通过 JSON 发送。所以,我需要一种方法来对它们进行字符串化。error.stringify()感觉比errorlib.stringify(error); 正如第二个结构所暗示的那样,我是在操作errorlib而不是在操作error本身。

回答by SoEzPz

If you look at it on a case by case basis, perhaps some implementations are acceptable.

如果您逐个查看它,也许某些实现是可以接受的。

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

Overwriting already created methods creates more issues than it solves, which is why it is commonly stated, in many programming languages, to avoid this practice. How are Devs to know the function has been changed?

覆盖已经创建的方法会产生比它解决的更多的问题,这就是为什么在许多编程语言中通常声明要避免这种做法。开发人员如何知道功能已更改?

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

In this case we are not overwriting any known core JS method, but we are extending String. One argument in this post mentioned how is the new dev to know whether this method is part of the core JS, or where to find the docs? What would happen if the core JS String object were to get a method named capitalize?

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

What if instead of adding names that may collide with other libraries, you used a company/app specific modifier that all the devs could understand?

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

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.

If you continued to extend other objects, all devs would encounter them in the wild with (in this case) we, which would notify them that it was a company/app specific extension.

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

This does not eliminate name collisions, but does reduce the possibility. If you determine that extending core JS objects is for you and/or your team, perhaps this is for you.

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

回答by Stefan

In my opinion, it's a bad practice. The major reason is integration. Quoting should.js docs:

在我看来,这是一种不好的做法。主要原因是整合。引用 should.js 文档:

OMG IT EXTENDS OBJECT???!?!@ Yes, yes it does, with a single getter should, and no it won't break your code

OMG IT EXTENDS OBJECT ???!?!@ 是的,是的,应该有一个 getter,不,它不会破坏你的代码

Well, how can the author know? What if my mocking framework does the same? What if my promises lib does the same?

好吧,作者怎么知道?如果我的模拟框架也这样做呢?如果我的承诺库也这样做呢?

If you're doing it in your own project then it's fine. But for a library, then it's a bad design. Underscore.js is an example of the thing done the right way:

如果你在自己的项目中这样做,那就没问题了。但是对于图书馆来说,这是一个糟糕的设计。Underscore.js 是一个以正确方式完成事情的例子:

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

回答by Eugen Wesseloh

One more reason why you should notextend native Objects:

应该扩展原生对象的另一个原因:

We use Magento which uses prototype.jsand extends a lot of stuff on native Objects. This works fine until you decide to get new features in and that's where big troubles start.

我们使用 Magento,它使用prototype.js并在原生对象上扩展了很多东西。这很好用,直到您决定引入新功能,这就是大麻烦开始的地方。

We have introduced Webcomponents on one of our pages, so the webcomponents-lite.js decides to replace the whole (native) Event Object in IE (why?). This of course breaks prototype.jswhich in turn breaks Magento. (until you find the problem, you may invest a lot of hours tracing it back)

我们在其中一个页面上引入了 Webcomponents,因此 webcomponents-lite.js 决定替换 IE 中的整个(原生)事件对象(为什么?)。这当然会破坏prototype.js,从而破坏Magento。(直到你发现问题,你可能会投入大量的时间来追溯)

If you like trouble, keep doing it!

如果你喜欢麻烦,那就继续吧!

回答by Eugen Wesseloh

Extending prototypes of built-ins is indeed a bad idea.However, ES2015 introduced a new technique that can be utilized to obtain the desired behavior:

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

Utilizing WeakMaps to associate types with built-in prototypes

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

The following implementation extends the Numberand Arrayprototypes without touching them at all:

以下实现扩展了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);

As you can see even primitive prototypes can be extended by this technique. It uses a map structure and Objectidentity to associate types with built-in prototypes.

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

My example enables a reducefunction that only expects an Arrayas its single argument, because it can extract the information how to create an empty accumulator and how to concatenate elements with this accumulator from the elements of the Array itself.

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

Please note that I could have used the normal Maptype, since weak references doesn't makes sense when they merely represent built-in prototypes, which are never garbage collected. However, a WeakMapisn't iterable and can't be inspected unless you have the right key. This is a desired feature, since I want to avoid any form of type reflection.

请注意,我可以使用普通Map类型,因为当弱引用仅表示从未被垃圾收集的内置原型时,它们没有意义。但是, aWeakMap不可迭代,除非您拥有正确的密钥,否则无法对其进行检查。这是一个理想的特性,因为我想避免任何形式的类型反射。

回答by Mark Amery

I can see three reasons not to do this (from within an application, at least), only two of which are addressed in existing answers here:

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

  1. If you do it wrong, you'll accidentally add an enumerable property to all objects of the extended type. Easily worked around using Object.defineProperty, which creates non-enumerable properties by default.
  2. You might cause a conflict with a library that you're using. Can be avoided with diligence; just check what methods the libraries you use define before adding something to a prototype, check release notes when upgrading, and test your application.
  3. You might cause a conflict with a future version of the native JavaScript environment.
  1. 如果你做错了,你会意外地为扩展类型的所有对象添加一个可枚举属性。使用 可以轻松解决Object.defineProperty,默认情况下会创建不可枚举的属性。
  2. 您可能会与正在使用的库发生冲突。可以通过勤奋避免;只需在向原型添加内容之前检查您使用的库定义了哪些方法,在升级时检查发行说明,并测试您的应用程序。
  3. 您可能会导致与本机 JavaScript 环境的未来版本发生冲突。

Point 3 is arguably the most important one. You can make sure, through testing, that your prototype extensions don't cause any conflicts with the libraries you use, because youdecide what libraries you use. The same is not true of native objects, assuming that your code runs in a browser. If you define Array.prototype.swizzle(foo, bar)today, and tomorrow Google adds Array.prototype.swizzle(bar, foo)to Chrome, you're liable to end up with some confused colleagues who wonder why .swizzle's behaviour doesn't seem to match what's documented on MDN.

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

(See also the story of how mootools' fiddling with prototypes they didn't own forced an ES6 method to be renamed to avoid breaking the web.)

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

This is avoidable by using an application-specific prefix for methods added to native objects (e.g. define Array.prototype.myappSwizzleinstead of Array.prototype.swizzle), but that's kind of ugly; it's just as well solvable by using standalone utility functions instead of augmenting prototypes.

这可以通过为添加到本机对象的方法使用特定于应用程序的前缀来避免(例如,定义Array.prototype.myappSwizzle而不是Array.prototype.swizzle),但这有点难看;通过使用独立的实用程序函数而不是增加原型,它也可以很好地解决。

回答by gman

Perf is also a reason. Sometimes you might need to loop over keys. There are several ways to do this

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

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

I usually use for of Object.keys()as it does the right thing and is relatively terse, no need to add the check.

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

But, it's much slower.

但是,它要慢得多

for-of vs for-in perf results

for-of 与 for-in 性能结果

Just guessing the reason Object.keysis slow is obvious, Object.keys()has to make an allocation. In fact AFAIK it has to allocate a copy of all the keys since.

只是猜测原因Object.keys很明显,Object.keys()必须进行分配。事实上 AFAIK 它必须分配所有密钥的副本。

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

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

It's possible the JS engine could use some kind of immutable key structure so that Object.keys(object)returns a reference something that iterates over immutable keys and that object.newPropcreates an entirely new immutable keys object but whatever, it's clearly slower by up to 15x

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

Even checking hasOwnPropertyis up to 2x slower.

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

The point of all of that is that if you have perf sensitive code and need to loop over keys then you want to be able to use for inwithout having to call hasOwnProperty. You can only do this if you haven't modified Object.prototype

所有这一切的重点是,如果您有性能敏感的代码并且需要循环键,那么您希望能够使用for in而无需调用hasOwnProperty. 只有在没有修改的情况下才能执行此操作Object.prototype