每个JS意见领袖都表示扩展原生对象是一种不好的做法。但为什么?我们是否获得了性能?他们是否害怕有人以“错误的方式”做到这一点,并将可枚举的类型添加到Object
,几乎会破坏任何对象上的所有循环?
以TJ Holowaychuk的should.js为例。他adds a simple getter到Object
,一切正常(source)。
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);
是否有任何反对扩展原生类型的论据?
答案 0 :(得分:102)
扩展对象时,可以更改其行为。
更改仅由您自己的代码使用的对象的行为很好。但是,当您更改其他代码也使用的某些内容的行为时,您可能会破坏其他代码。
当它在javascript中向对象和数组类添加方法时,由于javascript的工作方式,破坏某些东西的风险非常高。多年的经验告诉我,这种东西会导致javascript中出现各种可怕的错误。
如果您需要自定义行为,最好定义自己的类(可能是子类)而不是更改本机类。这样你根本不会破坏任何东西。
在没有子类化的情况下改变类的工作方式的能力是任何优秀编程语言的一个重要特征,但它必须很少使用并且要谨慎使用。
答案 1 :(得分:27)
没有可衡量的缺点,例如性能损失。至少没有人提到过。所以这是个人偏好和经历的问题。
主要赞成论点:它看起来更好,更直观:语法糖。它是一个特定于类型/实例的函数,因此它应该特定地绑定到该类型/实例。
主要反对论点:代码可能会干扰。如果lib A添加了一个函数,它可能会覆盖lib B的函数。这可以很容易地破解代码。
两者都有一点意义。当您依赖两个直接更改类型的库时,您很可能最终会遇到损坏的代码,因为预期的功能可能不一样。我完全同意这一点。宏库不得操纵本机类型。否则,作为开发人员,你永远不会知道幕后的真实情况。
这就是我不喜欢像jQuery,下划线等lib这样的原因。别误会我的意思;它们绝对是编程良好的,它们就像一个魅力,但它们是大。你只使用它们的10%,并且理解大约1%。
这就是为什么我更喜欢atomistic approach,你只需要你真正需要的东西。这样,你总能知道会发生什么。微库仅执行您希望他们执行的操作,因此它们不会干扰。在让最终用户知道添加了哪些功能的情况下,可以认为扩展本机类型是安全的。
TL; DR 如有疑问,请勿扩展原生类型。如果您100%确定,则只扩展本机类型,最终用户将了解并希望该行为。在 no 的情况下,操作本机类型的现有函数,因为它会破坏现有的接口。
如果您决定扩展类型,请使用Object.defineProperty(obj, prop, desc)
; if you can't,使用类型prototype
。
我最初想出了这个问题,因为我希望Error
可以通过JSON发送。所以,我需要一种方法来对它们进行字符串化。 error.stringify()
感觉比errorlib.stringify(error)
更好;正如第二个结构所暗示的那样,我正在errorlib
而不是error
本身。
答案 2 :(得分:13)
在我看来,这是一种不好的做法。主要原因是整合。引用should.js文档:
OMG IT EXTENDS OBJECT ???!?!@是的,是的,只需一个吸气剂 应该,不,它不会破坏你的代码
嗯,作者怎么知道?如果我的模拟框架做同样的事情怎么办?如果我的承诺lib也一样呢?
如果你在自己的项目中这样做,那就没事了。但对于图书馆来说,这是一个糟糕的设计。 Underscore.js是以正确方式完成的事情的一个例子:
var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
答案 3 :(得分:10)
如果你根据具体情况来看,也许可以接受一些实现。
String.prototype.slice = function slice( me ){
return me;
}; // Definite risk.
覆盖已经创建的方法会产生比它解决的问题更多的问题,这就是为什么在许多编程语言中通常会说明为避免这种做法。 Devs如何知道函数已被更改?
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 :(得分:7)
扩展内置插件的原型确实是一个坏主意。然而,ES2015引入了一种可用于获得所需行为的新技术:
WeakMap
将类型与内置原型相关联以下实现扩展了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
不可迭代,除非您拥有正确的密钥,否则无法检查。这是一个理想的功能,因为我想避免任何形式的反射。
答案 5 :(得分:3)
您应该不扩展本机对象的另一个原因:
我们使用Magento,它使用 prototype.js 并在本机对象上扩展了很多东西。 这种方法很好,直到您决定使用新功能,这就是大麻烦的开始。
我们在其中一个页面上引入了Webcomponents,因此webcomponents-lite.js决定替换IE中的整个(本机)事件对象(为什么?)。 这当然会破坏 prototype.js ,而这又打破了Magento。 (直到你发现问题,你可能会花很多时间追查它)
如果你喜欢麻烦,继续这样做吧!
答案 6 :(得分:1)
我可以看到三个不这样做的理由(至少来自应用程序),其中只有两个在现有答案中解决:
Object.defineProperty
轻松解决,默认情况下会创建不可枚举的属性。第3点可以说是最重要的一点。您可以通过测试确保您的原型扩展不会与您使用的库发生任何冲突,因为您决定使用哪些库。假设您的代码在浏览器中运行,原始对象也是如此。如果您今天定义Array.prototype.swizzle(foo, bar)
,明天Google将Array.prototype.swizzle(bar, foo)
添加到Chrome,您可能会遇到一些混淆的同事,他们想知道为什么.swizzle
的行为似乎不匹配MDN上记录了什么。
通过为添加到本机对象的方法使用特定于应用程序的前缀(例如,定义Array.prototype.myappSwizzle
而不是Array.prototype.swizzle
)可以避免这种情况,但这有点难看;通过使用独立的实用程序函数而不是扩充原型,它也是可以解决的。
答案 7 :(得分:0)
Perf也是一个原因。有时您可能需要遍历键。有几种方法可以做到这一点
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()
,因为它做对了,而且比较简洁,不需要添加支票。
但是,it's much slower。
只需猜测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
答案 8 :(得分:0)
我个人正在扩展本机方法,我只是在我的库中使用 x
前缀(在扩展第 3 方库时也使用它)。
(因为唯一的问题是与未来的标准相冲突,该标准很可能永远不会在本机方法中引入“x”前缀,后跟大写字母),例如
仅 TS:
declare global {
interface Array<T> {
xFlatten(): ExtractArrayItemsRecursive<T>;
}
}
JS+TS:
Array.prototype.xFlatten = function() { /*...*/ }