每个JS意见领袖都说扩展本机对象是一个糟糕的实践。但为什么呢?我们的表演受到打击了吗?他们是否担心有人做了"错误的方式",并向Object添加了可枚举类型,实际上破坏了任何对象上的所有循环?
以TJ Holowaychuk的should.js为例。他在Object中添加了一个简单的getter,一切都很好(source)。
1 2 3 4 5 6 7
| Object.defineProperty(Object.prototype, 'should', {
set: function(){},
get: function(){
return new Assertion(Object(this).valueOf());
},
configurable: true
}); |
这很有道理。例如,可以扩展Array。
1 2 3 4 5 6 7 8
| Array.defineProperty(Array.prototype,"remove", {
set: function(){},
get: function(){
return removeArrayElement.bind(this);
}
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3); |
号
是否有任何反对扩展本机类型的论点?
- 稍后,当本机对象被更改为包含一个具有不同语义的"remove"函数时,您希望发生什么?你不能控制标准。
- 这不是你的母语。这是每个人的本土化类型。
- 他们是否害怕有人以"错误的方式"执行此操作,并向对象添加可枚举类型,实际上会破坏任何对象上的所有循环?:是。在这个观点形成的年代,不可能创造出不可枚举的属性。现在,在这方面事情可能有所不同,但是想象一下每个库只是按照他们想要的方式扩展本机对象。我们开始使用名称空间是有原因的。
- 我也是这么想的。但是作为一个开发人员,你应该知道你实现了哪些框架,它们注册了哪些函数。没有人愿意重新创建uujs,以便将自己绑定到本机类型。这只适用于罕见的边缘案例。
- 对于一些"意见领袖",比如布兰登·艾奇(BrendanEich),认为扩展本地原型是非常好的选择。
- 一本好书:Perfectionkills.com/extending-native-builtins
- 好吧,那么扩展本机对象是不好的……那么当您构建一个具有构造函数函数的API(例如"foo")时会发生什么,然后ES-20xx规范决定引入foo?突然间,您完全用自己的foo覆盖了es-20xx规格的foo。在某种程度上,这不是所有的摇摆和旋转吗?
- 现在的foo不应该是全球性的,我们有include、requirejs、commonjs等。
- 作为一个吹毛求疵的人,"本地人"应该是"内置的"。ecma-262不再使用术语"本机",但它过去被定义为任何内置对象或通过运行代码创建的对象,因此var x = {}创建本机对象。例如,见ECMA-262第5版&167;4.3.6。
扩展对象时,会更改其行为。
更改只由您自己的代码使用的对象的行为是可以的。但是,当您更改其他代码也使用的某个代码的行为时,可能会破坏其他代码。
当它在javascript中向对象和数组类添加方法时,由于javascript的工作方式,破坏某些东西的风险非常高。多年的经验告诉我,这类东西会导致JavaScript中出现各种可怕的错误。
如果需要自定义行为,最好定义自己的类(可能是子类)而不是更改本机类。这样你就不会破坏任何东西。
在不进行子类化的情况下改变类的工作方式的能力是任何一种好的编程语言的一个重要特性,但它是一种必须很少使用且必须谨慎使用的功能。
- 因此,添加像EDOCX1[0]这样的内容是安全的吗?
- @席尔文西不,我不认为加任何东西是安全的。
- @abhibeckert根据lsp,扩展一个对象应该扩展行为(而不是完全改变它),这样该对象就可以与基对象在相同的上下文中使用。另外,你能详细阐述最后一段吗?
- @Stefan注意到尽管lsp上下文中的"extend"意味着"创建子类型",而不是"就地修改现有类型"。不过,这是一个公平的观点。
- @德尔南是真的,但我们只讨论了在原型中添加一些东西,而没有改变任何东西。从这个意义上说,我看不出这方面的问题,除非您需要集成库。代码可能更难理解。
- 有很多问题,例如,如果其他一些代码也试图添加它自己的stringify()方法,但行为不同,会发生什么?这真的不是你在日常编程中应该做的事情…如果你只想在这里和那里保存几个字符的代码,那就不需要了。最好定义自己的类或函数,接受任何输入并将其字符串化。大多数浏览器定义JSON.stringify(),最好是检查它是否存在,如果不自己定义。
- @stefan它通过使用赋值来"添加",它还将覆盖已经存储在该属性名下的任何内容。我们可以相对容易地避免这一点,但由于问题并没有预料到这一点,所以至少应该提到这一点。
- 我更喜欢someError.stringify()而不是errors.stringify(someError)。它非常简单,非常适合JS的概念。我正在做一些特定于某个错误对象的事情。
- 唯一(但很好)的论点是,一些冗长的宏库可能开始接管您的类型,并可能相互干扰。或者还有其他的吗?
- 你说得对,当你扩展本机对象的时候,它确实会创建很好的代码。但这不值得花这么多钱。丑陋的代码比错误的代码好。我不知道你认为什么是"冗长的宏库",但我所做的每件事都足够大,可以包含许多人编写的代码,甚至不是我见过的所有人,更不用说知道他们在未来对本机对象的行为做了或将要做的假设了。
- 我知道这很古老,但我不得不把我的两分钱放进去,因为评论和选择正确的答案震惊了我作为一个专业人士。moootools、原型和现代化工具都利用扩展本地对象的优势。他们增强了本土的行为,但并没有改变。>"丑陋的代码总比错误的代码好"。根据我的经验,丑陋的代码也很可能是错误的。扩展主要活动是完全可以接受的,本身并不是不好的做法。坏的做法是改变预期的行为…避免这样,这不仅是好的,这是经典的OOP!
- @是的,但moootools/etc正处于一个独特的位置,它可以延伸出一个以冰川速度移动的国际标准。实际实现Javascript中的提议更改需要5或6年时间。XMLHttpRequest最初由微软于1998年或1999年提出,并于2006年4月作为草案添加到标准中。微软于2006年10月将其添加到IE中。今天它仍然只是一个标准草案,最后一次修改是在2014年1月。谁知道什么时候才是真正的标准呢?
- @LanceCaraccioli-Apple的专有框架是不同的,他们每年都在世界开发者大会上对SDK进行向后不兼容的更改。每年都会有一长串中断的东西,如果你的代码受到影响,你会有几个月的测试期来更新你的代码来处理变更。经验丰富的obj-c程序员避免做可能遇到这个问题的事情,苹果官方建议您可以扩展本机对象,但应该尽可能避免。
- 如果我看起来很困惑,那是因为我给了你怀疑的好处。否则@abhi beckert js就是操作的上下文,苹果的专有框架不是操作的上下文,我会澄清。以意外和向后不兼容的方式更改全局资源的行为显然是不正确的。许多领先的JS库都会对这些对象进行扩充以添加额外的直观行为。更进一步。如果lib将array.push更改为array.pop,则不要使用它。否则,像mootools的object.toquerystring这样的扩展非常有用。
- @兰斯卡拉契奥利问题是,你添加的任何东西将来可能会被苹果添加,特别是如果它显然属于这一类。您的添加可能与苹果的名称相同,然后您的将替换苹果的,这将导致错误。与javascript不同的是,uikit广泛使用自己的类,因此,如果您改变一个uikit类的行为方式,您将以不可知的方式破坏许多其他类。我做obj-c已经10年多了,相信我——在uikit类中添加方法是危险的。
- 谁在谈论苹果?如果标准发生变化,您可能需要更新源代码。这不是目前的话题。
- 我发现自己同意@lancecaraccioli。如果您没有覆盖先前存在的方法的功能,那么添加您自己的方法是可以的。
- @谷物添加你自己的方法是好的,只要你的方法不是相同的名称,苹果在未来添加的任何方法,或已经秘密使用。最后一部分是问题所在,cocoa和cocoatouch都是存在的实例方法,但是没有公开记录在任何地方。如果在同一类上实现具有相同名称的方法,则方法将重写"secret"方法,而不会出现任何错误消息或警告,并且应用程序可能会继续工作,也可能不会继续工作。你也将被拒绝从应用商店。
- 下面是一个很快的真实例子,说明这会导致什么类型的错误(在控制台中试试这个):function printObj(a){ for(var i in a) { console.log("i->a:",[i,a[i]]) } }; arr=[1,2,3]; printObj(arr); console.log("------------"); Array.prototype.example = function(){console.log('hi')}; printObj(arr);,我用这个枪射自己的脚。您可能希望原型方法不会在迭代中显示为属性。
- @Balthatrix就是一个坏例子,因为它依赖于javascript中一个可怕的语言设计缺陷,而不会在任何其他语言中发生。
- @Abhibeckert-hmmm,我认为这个问题的范围局限于JS,你能不能扩展一下你的意思?设计/功能缺陷是什么?感谢您的反馈!
- @这个问题是关于JS的,但我的答案适用于所有语言。javascript有两个设计缺陷适用于这里1。它不能正确地分离代码和数据,所以您的可执行代码被视为数据(反过来,数据也被视为代码)。2。数组实现没有定义"适当"的数据结构,它只是使用用于所有对象的内置数据结构,这意味着数组不可能包含不属于数组的属性。这两个错误几乎不存在于任何其他语言中。
- 如果问题是您可能发生了冲突,您是否可以使用一个模式,在您每次执行此操作时,您总是在定义函数之前确认它是未定义的?如果您总是将第三方库放在自己的代码之前,那么您应该能够始终检测到此类冲突。
- @Sahuagin只会帮助你发现问题,但实际上并不能解决问题。根据函数的使用程度,重命名函数可能需要很多工作,特别是涉及到第三方库时。
没有可测量的缺点,比如性能下降。至少没人提到。所以这是个人偏好和经验的问题。
主要的支持论点是:它看起来更好而且更直观:语法糖。它是一个特定于类型/实例的函数,因此应该专门绑定到该类型/实例。
相反的主要论点是:代码可以干扰。如果lib a添加了一个函数,它可能会覆盖lib b的函数。这很容易破译代码。
两者都有道理。当您依赖两个直接更改类型的库时,很可能会以代码中断告终,因为预期的功能可能不一样。我完全同意。宏库不能操作本机类型。否则,作为一个开发人员,你将永远不知道幕后到底发生了什么。
这就是我不喜欢jquery、下划线等libs的原因。不要误会我;它们的编程非常好,而且它们的工作方式很有魅力,但是它们很大。你只使用其中的10%,并且了解大约1%。
这就是为什么我更喜欢原子论的方法,你只需要你真正需要的。这样,你总是知道会发生什么。微型图书馆只做你想让他们做的,所以他们不会干涉。在让最终用户知道添加了哪些功能的情况下,可以认为扩展本机类型是安全的。
当有疑问时,不要扩展本机类型。只有当您100%确定最终用户知道并希望该行为时,才扩展本机类型。在任何情况下都不能操作本机类型的现有函数,因为它会破坏现有接口。
如果决定扩展类型,请使用Object.defineProperty(obj, prop, desc);如果不能,请使用该类型的prototype。
我最初提出这个问题是因为我希望Error可以通过JSON发送。所以,我需要一种方法来把它们串起来。error.stringify()比errorlib.stringify(error)感觉好多了;正如第二个结构所示,我在errorlib上操作,而不是在Error上操作。
- 我对这方面的进一步看法持开放态度。
- 您是在暗示jquery和下划线扩展本机对象吗?他们没有。所以如果你因为这个原因而避开他们,你就错了。
- 在用lib a扩展本机对象可能与lib b冲突的论点中,我认为有一个问题遗漏了:为什么有两个库在性质上如此相似或在性质上如此广泛以致于可能发生这种冲突?也就是说,我要么选择lodash,要么在两者之间加下划线。我们当前在javascript中的libraray布局过于饱和,开发人员(通常)在添加它们时变得如此粗心大意,以至于我们最终避免了最佳实践来安抚我们的lib领主。
- @Micahblu——一个库可以决定修改一个标准对象,只是为了方便自己的内部编程(这常常是人们想要这样做的原因)。所以,这一冲突并不能避免,因为您不会使用两个具有相同功能的库。
- -1对于jquery和下划线扩展本机类型的含义(可能不是有意的),这是不正确的,而且因为我拒绝了这样一种观点,即只有不使用的第三方库函数才是有问题的。一方面,我可能比我的浏览器附带的本地API了解更多的lodash或jquery;另一方面,为什么仅仅因为存在一些我不使用的函数就要害怕呢?他们不会吃我的。
- 您还可以(从ES2015开始)创建一个扩展现有类的新类,并在您自己的代码中使用该类。因此,如果MyError extends Error可以有一个stringify方法,而不会与其他子类冲突。您仍然需要处理不是由您自己的代码生成的错误,因此使用Error可能比使用其他代码更不有用。
在我看来,这是一种不好的做法。主要原因是整合。引用should.js文档:
OMG IT EXTENDS OBJECT???!?!@ Yes, yes it does, with a single getter
should, and no it won't break your code
号
嗯,作者怎么知道?如果我的模拟框架也是这样呢?如果我的承诺Lib也这么做呢?
如果你是在自己的项目中做的,那就没问题了。但对于图书馆来说,这是一个糟糕的设计。underline.js是正确操作的一个例子:
1 2 3 4
| var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten() |
- 我相信TJ的回应将是不使用那些承诺或嘲笑框架:x
- _(arr).flatten()的例子实际上说服了我不要扩展本地对象。我个人这样做的原因纯粹是语法上的。但这满足了我的审美感受:)即使使用一些更规则的函数名,如foo(native).coolStuff()将其转换为一些"扩展"对象,在语法上也很不错。所以谢谢你!
If you look at it on a case by case basis, perhaps some implementations are acceptable.
号
1 2 3
| String.prototype.slice = function slice( me ){
return me;
}; // Definite risk. |
覆盖已经创建的方法会产生比它解决的问题更多的问题,这就是为什么在许多编程语言中,通常会说它是为了避免这种做法。开发人员如何知道功能已更改?
1 2 3
| String.prototype.capitalize = function capitalize(){
return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk. |
号
在这种情况下,我们不会覆盖任何已知的核心JS方法,但我们正在扩展字符串。本文中的一个参数提到了新开发人员如何知道这个方法是核心JS的一部分,还是在哪里找到文档?如果核心JS字符串对象得到一个名为capitalize的方法,会发生什么?
如果不添加可能与其他库冲突的名称,而是使用所有开发人员都能理解的特定于公司/应用程序的修饰符,该怎么办?
1 2 3 4 5 6 7
| 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将类型与内置原型关联起来
以下实现扩展了Number和Array原型,根本不接触它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| // 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]。江户十一〔四〕号
- @Naomik我知道-看看我最新的问题第二个代码片段。
- 支持@ftor的标准是什么?
- -1;这有什么意义?您只是创建一个单独的全局字典,将类型映射到方法,然后按类型显式地查找字典中的方法。因此,您将失去对继承的支持(以这种方式"添加"到Object.prototype的方法不能在Array上调用),并且语法比真正扩展原型的语法要长/丑得多。用静态方法创建实用程序类几乎总是简单的;这种方法的唯一优点是对多态性的支持有限。
- @马卡梅里嘿,伙计,你不明白。我不失去遗产,但把它扔掉。继承权是80年代的,你应该忘记它。这种黑客攻击的关键是模仿类型类,简单地说,类型类是重载函数。不过,有一种合理的批评:在javascript中模仿类型类是否有用?不,不是。而是使用一种本机支持它们的类型化语言。我很确定这就是你的意思。
不应扩展本机对象的另一个原因是:
我们使用magento,它使用prototype.js并在本机对象上扩展了许多内容。这很好,直到你决定加入新功能,这就是大麻烦的开始。
我们在其中一个页面上引入了WebComponents,因此webcomponents-lite.js决定替换IE中的整个(本机)事件对象(为什么?).这当然会破坏prototype.js,反过来又会破坏magento。(在你发现问题之前,你可能会花很多时间来追踪它)
如果你喜欢麻烦,继续做吧!
我可以看到三个不这样做的原因(至少在一个应用程序中),在这里的现有答案中只有两个:
如果操作错误,则会意外地向扩展类型的所有对象添加一个可枚举属性。很容易使用Object.defineProperty,这在默认情况下创建了不可枚举的属性。
您可能会与正在使用的库发生冲突。可以通过勤奋避免;只需在向原型添加内容之前检查您使用的库定义的方法,在升级时检查发行说明,并测试您的应用程序。
您可能会导致与本机JavaScript环境的未来版本发生冲突。
第3点可以说是最重要的一点。通过测试,您可以确保原型扩展不会与您使用的库产生任何冲突,因为您可以决定使用什么库。假定代码在浏览器中运行,则本机对象的情况也不一样。如果你今天定义Array.prototype.swizzle(foo, bar),明天谷歌将Array.prototype.swizzle(bar, foo)添加到chrome中,你可能会遇到一些困惑的同事,他们想知道为什么.swizzle的行为似乎与MDN中记录的不符。
(另请参见MooTools如何摆弄自己没有的原型的故事,强制重命名ES6方法以避免破坏Web。)
对于添加到本机对象中的方法(例如,定义Array.prototype.myappSwizzle而不是Array.prototype.swizzle),使用特定于应用程序的前缀是可以避免的,但这有点难看;使用独立的实用程序函数而不是增强原型也是可以解决的。