为什么扩展本地对象是一个不好的做法?
每个JS意见领袖都说扩展本地对象是一个不好的做法。 但为什么? 我们得到一个perfomance命中? 他们是否担心有人这样做是“错误的方式”,并将可枚举的types添加到Object
,实际上会摧毁任何对象上的所有循环?
以TJ Holowaychuk的should.js为例。 他添加了一个简单的getter 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);
有没有反对扩大本地types的任何争论?
当你扩展一个对象时,你改变它的行为。
改变一个只会被你自己的代码使用的对象的行为是好的。 但是当你改变其他代码所使用的某些东西的行为时,你就会冒这个代码的风险。
当在javascript中为对象和数组类添加方法时,由于javascript的工作方式,造成破坏的风险非常高。 多年的经验告诉我,这种东西会导致JavaScript中的各种可怕的错误。
如果您需要自定义行为,定义自己的类(可能是一个子类)而不是更改本机类会好得多。 这样你就不会破坏任何东西。
能够改变一个类的工作方式而不用inheritance它是任何好的编程语言的一个重要特征,但它是一个必须谨慎使用的方法。
没有可衡量的缺点,就像性能受到打击一样。 至less没有人提到过。 所以这是一个个人喜好和经验的问题。
主要的亲参数:它看起来更好,更直观:语法糖。 它是一个types/实例特定的函数,所以它应该特别绑定到该types/实例。
主要矛盾的论点:代码可以干涉。 如果lib A添加了一个函数,它可能会覆盖lib B的函数。 这可以很容易地破坏代码。
两者都有一个观点。 当你依赖两个直接改变你的types的库时,你很可能会得到破坏的代码,因为预期的function可能不一样。 我完全同意这一点。 macros库不能操纵本地types。 否则,作为开发人员,你永远不会知道幕后发生了什么。
这就是我不喜欢像jQuery,下划线等库。不要误解我的意思。 他们绝对精心编程,他们的工作就像一个魅力,但他们是大的 。 你只用了10%,理解了1%左右。
这就是为什么我更喜欢primefaces的方法 ,你只需要你真正需要的东西。 这样,你总是知道发生了什么。 微型图书馆只做你想让他们做的事情,所以他们不会干涉。 在最终用户知道哪些function被添加的情况下,扩展本地types可被认为是安全的。
TL; DR如果有疑问,请不要扩展原生types。 如果你100%确定,只扩展一个本地types,那么最终用户将会知道并且想要这个行为。 在任何情况下操纵本地types的现有function,因为它会打破现有的接口。
如果你决定扩展types,使用Object.defineProperty(obj, prop, desc)
; 如果你不能 ,使用types的prototype
。
我最初提出这个问题,因为我想通过JSON发送Error
。 所以,我需要一种方法将它们串联起来。 error.stringify()
感觉比errorlib.stringify(error)
更好; 正如第二个构造所暗示的那样,我正在使用errorlib
而不是error
本身。
在我看来,这是一个不好的做法。 主要原因是一体化。 引用should.js文档:
OMG IT EXTENDS OBJECT ???!?!@是的,是的,它有一个单一的getter,不,它不会破坏你的代码
那么,作者怎么知道呢? 如果我的嘲笑框架也一样呢? 如果我的承诺lib做同样的事情呢?
如果你在自己的项目中做,那就没问题了。 但对于一个图书馆来说,这是一个糟糕的devise。 Underscore.js就是一个正确的方法:
var arr = []; _(arr).flatten() // or: _.flatten(arr) // NOT: arr.flatten()
如果你在个案的基础上来看它,也许有些实现是可以接受的。
String.prototype.slice = function slice( me ){ return me; }; // Definite risk.
覆盖已经创build的方法会产生比解决问题更多的问题,这就是为什么在许多编程语言中通常会说这种方法来避免这种做法。 Devs如何知道function已经改变?
String.prototype.capitalize = function capitalize(){ return this.charAt(0).toUpperCase() + this.slice(1); }; // A little less risk.
在这种情况下,我们不覆盖任何已知的核心JS方法,但是我们正在扩展String。 在这篇文章中的一个论点提到新的开发者是如何知道这个方法是核心JS的一部分,还是在哪里find文档? 如果核心的JS String对象得到一个名为大写的方法会发生什么?
如果不是添加可能与其他库冲突的名称,而是使用了所有开发人员都能理解的公司/应用特定修改器?
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对象适合你和/或你的团队,也许这是给你的。
扩展内置的原型确实是一个坏主意。 然而,ES2015引入了一种新技术,可以用来获得所需的行为:
使用WeakMap
将types与内置原型相关联
下面的实现扩展了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);