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

bus*_*ens 121 javascript prototype prototypal-inheritance

每个JS意见领袖都说扩展原生对象是一种不好的做法.但为什么?我们是否获得了性能?他们是否害怕有人以"错误的方式"做到这一点,并添加了可枚举的类型Object,几乎破坏了任何对象上的所有循环?

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

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

这真的很有道理.例如,可以扩展Array.

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

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

Abh*_*ert 112

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

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

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

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

在没有子类化的情况下改变类的工作方式的能力是任何优秀编程语言的一个重要特征,但它必须很少使用并且要谨慎使用.

  • 我知道这是旧的,但我不得不把我的两分钱,因为评论和选择正确的答案震惊了我作为一个专业人士.Mootools,Prototype和modernizer都利用扩展本机对象.他们增加了他们不改变它的原生行为.>"丑陋的代码比错误的代码更好".根据我的经验,丑陋的代码最有可能也是错误的.扩展灵长类动物是完全可以接受的,而不是本身的不良实践.不好的做法是改变预期的行为......避免这种情况,这不仅仅是经典的OOP! (9认同)
  • 有很多问题,例如,如果其他一些代码也试图用不同的行为添加它自己的`stringify()`方法会发生什么?这不是你应该在日常编程中做的事情......如果你想做的就是在这里和那里保存几个代码字符.最好定义自己的类或函数,接受任何输入并对其进行字符串化.大多数浏览器定义`JSON.stringify()`,最好检查是否存在,如果不自己定义它. (5认同)
  • 我更喜欢`someError.stringify()`而不是`errors.stringify(someError)`.它简单明了,非常适合js的概念.我正在做一些特定绑定到某个ErrorObject的东西. (3认同)
  • 这是一个关于这可能导致哪种错误的快速真实示例(在控制台中尝试这个): `function printObj(a){ for(var i in a) { console.log("i->a: ",[i,人工智能]]) } }; arr=[1,2,3]; printObj(arr); console.log("------------"); Array.prototype.example = function(){console.log('hi')}; printObj(arr);` ^我用这个搬起石头砸了自己的脚。您可能希望原型方法不会在迭代中作为属性出现。 (3认同)
  • 如果问题是您可能发生冲突,那么是否可以在每次执行此操作时都使用一种模式,在定义该函数之前始终确认该函数未定义?而且,如果始终将第三方库放在自己的代码之前,则应该能够始终检测到此类冲突。 (3认同)
  • 因此,添加类似`.stgringify()`之类的内容将被视为安全吗? (2认同)

bus*_*ens 30

没有可测量的缺点,如性能损失.至少没有人提到过.所以这是一个个人偏好和经验的问题.

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

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

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

这就是我不喜欢像jQuery,下划线等lib这样的原因.别误会我的意思; 它们绝对是编程良好的,它们的工作就像一个魅力,但它们很大.您只使用其中的10%,并了解约1%.

这就是为什么我更喜欢原子论的方法,你只需要你真正需要的东西.这样,你总能知道会发生什么.微库仅执行您希望他们执行的操作,因此它们不会干扰.在让最终用户知道添加了哪些功能的上下文中,可以认为扩展本机类型是安全的.

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

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


我最初想出了这个问题,因为我想Error通过JSON发送.所以,我需要一种方法来对它们进行字符串化.error.stringify()感觉好多了errorlib.stringify(error); 正如第二种结构所表明的那样,我正在经营errorlib而不是error自身.

  • 你是暗示jQuery和下划线扩展本机对象?他们没有.因此,如果你因为这个原因而避开它们,那你就错了. (3认同)
  • @micahblu - 一个库可以决定修改一个标准对象只是为了方便它自己的内部编程(这通常是人们想要这样做的原因)。因此,不会仅仅因为您不会使用具有相同功能的两个库而避免这种冲突。 (3认同)
  • 这是我认为在使用 lib A 扩展本机对象可能与 lib B 冲突的论点中遗漏的一个问题:为什么您有两个在性质上如此相似或性质如此广泛以至于可能发生这种冲突的库?即我要么选择 lodash 要么选择下划线而不是两者。我们目前在 javascript 中的图书馆景观过于饱和,开发人员(通常)在添加它们时变得如此粗心,以至于我们最终避免了安抚我们的自由主义者的最佳实践 (2认同)
  • 您还可以(从 es2015 开始)创建一个扩展现有类的新类,并在您自己的代码中使用它。因此,如果“MyError 扩展了 Error”,它可以具有“stringify”方法,而不会与其他子类发生冲突。您仍然必须处理不是由您自己的代码生成的错误,因此“Error”可能不如其他错误有用。 (2认同)

Ste*_*fan 14

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

OMG IT EXTENDS OBJECT ???!?!@是的,是的,确实如此,一个getter应该,并且它不会破坏你的代码

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

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

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Run Code Online (Sandbox Code Playgroud)

  • `_(arr).flatten()` 示例实际上说服了我不要扩展原生对象。我个人这样做的原因纯粹是语法上的。但这满足了我的美感:)即使使用一些更常规的函数名称,如`foo(native).coolStuff()`将其转换为一些“扩展”对象,在语法上看起来也很棒。所以谢谢你! (3认同)
  • 我确信TJ的回应是不使用这些承诺或模拟框架:x (2认同)

SoE*_*zPz 12

如果你根据具体情况来看,也许一些实现是可以接受的.

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.
Run Code Online (Sandbox Code Playgroud)

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

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

在这种情况下,我们不会覆盖任何已知的核心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.
Run Code Online (Sandbox Code Playgroud)

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

这并不能消除名称冲突,但确实降低了可能性.如果您确定扩展核心JS对象是为您和/或您的团队,也许这适合您.


小智 8

扩展内置插件的原型确实是一个坏主意.但是,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);
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,甚至可以通过这种技术扩展原始原型.它使用地图结构和Object标识将类型与内置原型相关联.

我的例子启用了一个reduce函数,它只需要一个Arrayas作为它的单个参数,因为它可以提取如何创建一个空累加器的信息,以及如何从Array本身的元素中将这个元素与这个累加器连接起来.

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

  • @MarkAmery嘿朋友,你不明白。我不会失去继承,但要摆脱它。继承是如此80年代,您应该忘记它。破解的全部目的是模仿类型类,简而言之就是重载函数。但是,有一个合理的批评:模仿Java语言中的类型类是否有用?不,不是。而是使用本地支持它们的类型化语言。我很确定这就是您的意思。 (3认同)
  • -1; 这有什么意义?您只是创建一个单独的全局字典将类型映射到方法,然后按类型在该字典中显式查找方法。结果,您失去了对继承的支持(不能在Array上调用以这种方式“添加”到Object.prototype的方法),并且语法要比真正扩展a的语法长/丑。原型。仅使用静态方法创建实用程序类几乎总是比较简单。这种方法的唯一优点是对多态性的支持有限。 (2认同)

Mar*_*ery 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),但这有点难看;通过使用独立的实用程序函数而不是增加原型,它也可以很好地解决。


小智 5

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

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

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

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