bus*_*ens 121 javascript prototype prototypal-inheritance
每个JS意见领袖都说扩展原生对象是一种不好的做法.但为什么?我们是否获得了性能?他们是否害怕有人以"错误的方式"做到这一点,并添加了可枚举的类型Object,几乎破坏了任何对象上的所有循环?
以TJ Holowaychuk的should.js为例.他增加了一个简单的getter来Object,一切工作正常(来源).
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中出现各种可怕的错误.
如果您需要自定义行为,最好定义自己的类(可能是子类)而不是更改本机类.那样你就不会破坏任何东西.
在没有子类化的情况下改变类的工作方式的能力是任何优秀编程语言的一个重要特征,但它必须很少使用并且要谨慎使用.
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自身.
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)
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将类型与内置原型相关联下面的实现扩展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);Run Code Online (Sandbox Code Playgroud)
正如您所看到的,甚至可以通过这种技术扩展原始原型.它使用地图结构和Object标识将类型与内置原型相关联.
我的例子启用了一个reduce函数,它只需要一个Arrayas作为它的单个参数,因为它可以提取如何创建一个空累加器的信息,以及如何从Array本身的元素中将这个元素与这个累加器连接起来.
请注意,我可以使用普通Map类型,因为弱引用只是代表内置原型时没有意义,它们从不被垃圾收集.但是,a WeakMap不可迭代,除非您有正确的密钥,否则无法检查.这是一个理想的功能,因为我想避免任何形式的类型反射.
我可以看到三个不这样做的原因(至少从应用程序中),其中只有两个在现有答案中得到解决:
Object.defineProperty,默认情况下会创建不可枚举的属性。第 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。(直到发现问题,您可能需要花费大量时间来追查问题)
如果您喜欢麻烦,请继续这样做!