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

以TJ Holowaychuk的should.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);


是否有任何反对扩展本机类型的参数?

评论

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

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

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

对于某些“意见领袖”来说,这是值得的,例如Br​​endan Eich认为扩展本机原型是一个很好的选择。

Foo如今不应该是全球性的,我们包括include,requirejs,commonjs等。

#1 楼

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

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

在向javascript中的对象和数组类添加方法时,这种风险由于javascript的工作原理,中断某件事的可能性非常高。多年的经验告诉我,这类问题会导致javascript中各种可怕的错误。

如果您需要自定义行为,最好定义自己的类(也许是子类)改变一个本地的。这样一来,您就不会破坏任何内容。

在不继承类的情况下更改类的工作方式的功能是任何好的编程语言的一项重要功能,但这是很少使用的语言,注意。

评论


因此,添加.stgringify()之类的内容将被视为安全吗?

–布希
2012年12月25日在22:21

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

–阿比·贝克特(Abhi Beckert)
2012-12-25 22:57



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

–布希
2012-12-26 3:50

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

–布希
2012年12月26日下午3:53

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

–戴夫·库西诺(Dave Cousineau)
16-09-22在16:21



#2 楼

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

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

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

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

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

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

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

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


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

评论


我对此有更多意见。

–布希
2012年12月26日4:27



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

– JLRishe
15年1月18日在20:41

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

– micahblu
15年5月12日在17:29

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

– jfriend00
2015年7月7日在3:44



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

–ShadSterling
18 Mar 20 '18 at 14:10

#3 楼


如果逐案研究,也许某些实现是可以接受的。


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.


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

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

#4 楼

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


OMG扩展对象???!?!@@是的,它确实如此,只有一个吸气剂
应该,不,它不会。破坏代码


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

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

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


评论


我敢肯定,TJ的回应将是不使用那些承诺或嘲笑框架:x

–吉姆·舒伯特(Jim Schubert)
13年8月8日在3:19

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

–egst
18-10-27在21:55



#5 楼

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

我们使用Magento,它使用了prototype.js并在本机对象上扩展了很多东西。
这很好,直到您决定获取新的对象

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

如果喜欢麻烦,继续做吧!

#6 楼

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

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

以下实现扩展了Number和完全不接触它们的Array原型:




 // 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函数,该函数仅期望将Array作为其单个参数,因为它可以提取如何创建信息。一个空的累加器,以及如何从数组本身的元素中使用此累加器连接元素。

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

评论


这是WeakMap的不错用法。请小心在空数组tho上使用xs [0]。类型(未定义).monoid ...

–谢谢
16-10-4在18:06

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

–user6445533
16-10-4在18:10

支持此@ftor的标准如何?

–onassar
17年11月18日在9:52

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

– Mark Amery
17/12/29在18:26

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

–user6445533
17年12月29日在19:20



#7 楼

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


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

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

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

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

#8 楼

性能也是一个原因。有时您可能需要遍历键。有几种方法可以做到这一点
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
5月19日13:38

但是由于其他原因,这仍然是一种不好的做法;)

– gman
5月19日14:37