155

每个 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);

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

4

9 回答 9

145

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

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

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

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

在不继承类的情况下改变类的工作方式的能力是任何好的编程语言的一个重要特性,但它必须很少使用并且必须谨慎使用。

于 2012-12-25T21:51:19.463 回答
37

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

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

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

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

这就是我不喜欢 jQuery、下划线等库的原因。不要误会我的意思;它们绝对是经过精心编程的,它们工作起来就像一个魅力,但它们很大。你只使用了其中的 10%,并且了解了大约 1%。

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

TL;DR如有疑问,请不要扩展本机类型。仅在您 100% 确定最终用户会知道并想要该行为的情况下扩展本机类型。在任何情况下都不能操纵本机类型的现有功能,因为它会破坏现有接口。

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


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

于 2012-12-26T04:24:10.690 回答
20

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

天哪,它扩展了对象???!?!@是的,是的,它确实,只有一个吸气剂应该,不,它不会破坏你的代码

那么,作者怎么会知道呢?如果我的模拟框架做同样的事情怎么办?如果我的 promises lib 也一样怎么办?

如果您在自己的项目中这样做,那很好。但是对于图书馆来说,这是一个糟糕的设计。Underscore.js 是正确完成事情的一个例子:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
于 2012-12-25T22:26:09.653 回答
18

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

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 对象适合您和/或您的团队,也许这适合您。

于 2015-07-16T17:57:23.580 回答
9

扩展内置的原型确实是个坏主意。但是,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仅期望 anArray作为其单个参数的函数,因为它可以从数组本身的元素中提取如何创建空累加器以及如何将元素与该累加器连接的信息。

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

于 2016-10-01T16:03:42.393 回答
8

不应扩展本机对象的另一个原因:

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

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

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

于 2018-05-02T11:51:27.810 回答
5

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

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

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

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

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

于 2017-12-29T18:59:46.593 回答
3

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

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()它做正确的事情并且相对简洁,无需添加检查。

但是,它要慢得多

for-of vs for-in 性能结果

只是猜测原因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 一样受欢迎,否则浏览器供应商不太可能解决你造成的问题。如果您的库确实如此受欢迎,那么强迫其他人解决您引起的问题将是一种卑鄙的做法。所以,请不要扩展本机对象

于 2019-11-03T03:38:36.573 回答
0

编辑:

一段时间后我改变了主意 -原型污染很糟糕(但是我在帖子末尾留下了示例)。

它可能会导致比上面和下面的帖子中提到的更多的麻烦。

在整个 JS/TS 领域拥有单一标准很重要(拥有一致的 npmjs 会很好)。

以前我写过 Bull**it 并鼓励人们在他们的库中这样做 - 对此感到抱歉:

Jeff Clayton 的提议似乎也不错 - 方法名称前缀可以是您的包名称,后跟下划线,例如:( Array.prototype.<custom>_Flatten不存在的包前缀可能会成为未来的现有包)


部分原始答案:

我个人在扩展本机方法,我只是x在我的库中使用前缀(在扩展 3rd 方库时也使用它)。

仅限 TS:

declare global {
  interface Array<T> {
    xFlatten(): ExtractArrayItemsRecursive<T>;
  }
}

JS+TS:

Array.prototype.xFlatten = function() { /*...*/ }
于 2021-07-02T12:17:13.883 回答