Most built-in JavaScript types are constructors whose prototypes contain the methods and other properties that define their default behavior:

  1. //(results will vary by browser)
  2.  
  3. Object.getOwnPropertyNames(Function.prototype)
  4. //["bind", "arguments", "toString", "length", "call", "name", "apply", "caller", "constructor"]

You can’t delete or replace a native prototype, but you can edit the values of its properties, or create new ones:

  1. //create a new array method that removes a member
  2. Array.prototype.remove = function(member) {
  3. var index = this.indexOf(member);
  4. if (index > -1) {
  5. this.splice(index, 1);
  6. }
  7. return this;
  8. }
  9.  
  10. ['poppy', 'sesame', 'plain'].remove('poppy'); //["sesame", "plain"]
  11. ['ant', 'bee', 'fly'].remove('spider'); //["ant", "bee", "fly"]

Et voila! Our code gets a useful array extension for free. However if you brag about doing this in production code, expect to get pummeled by a wave of fierce disapproval. Some of it carries weight. Let’s sift the danger from the dogma and try to reach an honest conclusion:

The Opposition

In recent years, multiple criticisms have been leveled against native prototype extension. Here’s an overview:

1. Future-proofing

If future browser versions implement Array.prototype.remove (either because of an upgrade to the EcmaScript standard, or through their own volition), their implementation will be overridden by our custom one, which will not only be less efficient (we can’t manipulate browser engine internals in the service of method optimization) but more importantly, they might have a different, non standard outcome.

A case in point: back in 2005 the Prototype.js framework implemented Function.prototype.bind. Four years later, the Ecma-262 committee (inspired by Prototype.js) included Function.prototype.bind in their ES 5 specification. Unfortunately for Prototype.js users, the new ES 5 standard required additional functionality, which was not supported by the elegantly simple Prototype.js version — for example ES 5 specifies that when a bound function is used as the first operand of instanceof, the internal [[HasInstance]] method should check the prototype chain of the original (or target) function.

  1. var myObj = {};
  2. var A = function() {};
  3. var ABound = A.bind(myObj);
  4.  
  5. (new ABound()) instanceof A;
  6. //true (in browsers which faithfully implement ES5 bind)
  7. //false (in the same browsers but with prototype.js loaded)

Similarly, software that makes use of third-party libraries runs the risk that a native prototype augmentation (home-grown or third-party) could be clobbered (or clobber) an alternate implementation of the same property by another library.

These concerns can be partially mitigated by checking for the existence of a native property before implementing it:

  1. Array.prototype.remove = Array.prototype.remove || function(member) {
  2. var index = this.indexOf(member);
  3. if (index > -1) {
  4. this.splice(index, 1);
  5. }
  6. return this;
  7. }

This solution depends on simultaneous adoption of new functionality across browsers. If the Chrome browser implemented Array.prototype.remove first, then all other browsers would still fall back on the home-grown implementation which may do something entirely different. For the same reason Prototype.js would have a problem with this strategy: since Array.prototype.bind is not implemented in IE versions 8 and earlier, those browsers would fall back on Prototype.js’s more limited functionality.

NOTE: as of Prototype 1.7.1, all functions that are also defined by ES 5 should be compliant with that specification

2. The for in loop

A secondary grumble, commonly heard but harder to justify, is that extending natives messes with the object iteration cycle. The argument goes like this: since for in loops will visit all enumerable properties in the object’s prototype chain, custom native properties will unexpectedly be included in such iterations:

  1. Object.prototype.values = function() {
  2. //etc..
  3. };
  4.  
  5. //later..
  6. var competitors = [];
  7. var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
  8. for (var prop in results) {
  9. competitors[competitors.length] = prop;
  10. }
  11.  
  12. competitors; //["Mary", "Ana", "Evelyn", "values"]!!

There are several reasons to suggest this fear is overblown. First off, the hasOwnProperty method can be used to filter out inherited properties.

  1. var competitors = [];
  2. var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
  3. for (var prop in results) {
  4. results.hasOwnProperty(prop) && competitors.push(prop);
  5. }
  6.  
  7. competitors; //["Mary", "Ana", "Evelyn"]

Second, ES 5 allows properties to be designated as non-enumerable and therefore immune from for in iteration:

  1. //supporting browsers only (not IE version 8 and earlier)
  2. Object.defineProperty(
  3. Object.prototype, 'values', {enumerable: false});
  4.  
  5. var competitors = [];
  6. var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
  7. for (var prop in results) {
  8. competitors[competitors.length] = prop;
  9. }
  10.  
  11. competitors; //["Mary", "Ana", "Evelyn"]

By the way, there is no reason* to use a for in statement to iterate arrays — for and while loops offer more convenience, flexibility and certainty — so pollution of for in loops should be a practical concern only when extending Object.prototype.

(*OK, almost no reason – never say never in JavaScript – in the unlikely event that you are burdened by an array which is sparse enough to cause a significant performance overhead – we’re talking very sparse here – then iterating with a for in loop will probably help. But even then, using hasOwnProperty will shield you from inherited enumerables.)

3. Shadowing

When it comes to extending Object.prototype (as opposed to native objects in general) there’s another reason to be wary. Descendants of Object.prototype (i.e. every object whose prototype is not explicitly null) will lose access to the extended property if they happen to define a property with the same name:

  1. Object.prototype.archive = function() {
  2. //etc..
  3. }
  4.  
  5. var concerto = {
  6. composer: 'Mozart',
  7. archive: 'K. 488'
  8. }
  9.  
  10. concerto.archive();
  11. //TypeError: Property 'archive' of object #<Object> is not a function

Each time we define a property on Object.prototype we are, in effect, generating an ad hoc reserved term, which is especially perilous when working with objects that pre-date the extension, or libraries we don’t own.

Extending Object.prototype “is Verboten”¹

For some or all of these reasons, the JavaScript community has considered Object.prototype extensions taboo for several years, and you’re very unlikely to see such extensions in production code or respected frameworks. I won’t tell you never to augment Object.prototype but I will tell you that doing so will make you a social pariah.

¹Title borrowed from this namesake article from 2005

What about Host Objects?

Host objects are vendor specific objects that are not covered by the ES standard — principally DOM objects such as Document, Node, Element and Event. Such objects are not well defined by any standard (the W3C standards — including HTML5 — merely talk of interfaces for DOM objects but do not require the existence of specific DOM constructors) and trying to lay ordered extensions on top of officially sanctioned chaos is a recipe for serial headaches.

For more on the perils of extending DOM objects see this fine article by @kangax.

So is Extending Natives ever okay?

I’ve described some reasons for not augmenting native prototypes; you may know of others. You need to decide whether each of these concerns will be addressed by your planned extension, and whether the extension would add power and clarity to your codebase.

Code shims (also known as polyfills) present a good case for extending natives. A shim is a chunk of code designed to reconcile behavioral differences across environments, by supplying missing implementations. ES 5 support is patchy in older browsers, in particular IE version 8 (and earlier), which can be frustrating for developers who want to take advantage of the latest ES 5 features (such as Function.prototype.bind and the higher order array functions: forEach, map, filter etc.) but also need to support these older browsers. Here’s an extract from a popular ES 5 shim (with the comments removed):

  1. //see https://github.com/kriskowal/es5-shim
  2.  
  3. if (!Array.prototype.forEach) {
  4. Array.prototype.forEach = function forEach(fun /*, thisp*/) {
  5. var self = toObject(this),
  6. thisp = arguments[1],
  7. i = -1,
  8. length = self.length >>> 0;
  9.  
  10. if (_toString(fun) != '[object Function]') {
  11. throw new TypeError(); // TODO message
  12. }
  13.  
  14. while (++i < length) {
  15. if (i in self) {
  16. fun.call(thisp, self[i], i, self);
  17. }
  18. }
  19. };
  20. }

The first statement checks if Array.prototype.forEach is already implemented and bails if it is. Our other bases are covered too: all properties added to native prototypes are defined by the ES 5 standard so its safe to assume they will not collide with unrelated namesake properties in the future; no ES 5 property extends Object.prototype so pollution of for in enumerations should not occur; every ES 5 property is well documented so there is no reason for ambiguity as to how the shim should be implemented and it’s clear which names are effectively reserved by the ES 5 standard (“bind”, “forEach” etc.).

Shimming ES 5 extensions makes a lot of sense. Without them we’re hostage to the inadequacies of lesser browsers and unable to take advantage of the language’s standard utility set. Yes, we can make use of the equivalent functionality offered by well written libraries like underscore.js, but still we’re locked into non-standard, inverted signatures in which methods are static and objects are merely extra arguments – an ungainly arrangement for an instance-only language. At some point all supported browsers will be ES 5 compliant, at which point the shimmed codebase can simply remove it’s shim library and carry on, while the unshimmed one must choose between a major refactor or a perpetually non-standard and static utility library.

NOTE: It’s not all a bed of roses. Some ES 5 methods are impossible to implement correctly using JavaScript in older browsers and must either fail silently or throw an exception. Others (such asFunction.prototype.bind) have a lot of edge cases that take many code iterations to get right. As Kris Kowal says of his own ES 5 shim library “As closely as possible to ES5 is not very close. Many of these shims are intended only to allow code to be written to ES5 without causing run-time errors in older engines. In many cases, this means that these shims cause many ES5 methods to silently fail. Decide carefully whether this is what you want.”

And then there’s one last thing to worry about…

4. What if everyone did it?

Should you decide its okay to augment a native prototype, another problem arises: other library providers might reach the same conclusion. Care must be taken not to include libraries whose prototype extensions collide with yours; the safest solution is to let only one framework (either your base codeline, or an included library) play the role of native extender. In the case of ES shims this should not be hard; you’re unlikely to write the shim yourself so just make sure that only one external shim library is included.

Sandboxing

What if we could have our own private Array, String or Function object that we could extend and use on demand, without messing up the global version? As @jdalton explains, there are various techniques for creating sandboxed natives, the most browser-neutral one uses an IFRAME:

  1. //Rough and ready version to illustrate technique
  2. //For production-ready version see http://msdn.microsoft.com/en-us/scriptjunkie/gg278167
  3. var sb, iframe = document.createElement('IFRAME');
  4. document.body.appendChild(iframe);
  5. sb = window.frames[1];
  6.  
  7. //later...
  8. sb.Array.prototype.remove = function(member) {
  9. var index = this.indexOf(member);
  10. if (index > -1) {
  11. this.splice(index, 1);
  12. }
  13. return this;
  14. }
  15.  
  16. //much later...
  17. var arr = new sb.Array('carrot', 'potato', 'leek');
  18. arr.remove('potato');
  19. arr; //['carrot', 'leek']
  20.  
  21. //global array is untouched
  22. Array.prototype.remove; //undefined

Sandboxed natives, when written well, offer safe cross-browser replications of native extensions. They’re a decent compromise but a compromise just the same. After all, the power of prototoype extensions is in their ability to modify all instances of a given type and provide each of them with access to the same behaviour set. With sandboxing we are required to know which of our array instances are “super-arrays” and which are native. Bugs love such uncertainties. It’s also unfortunate that sandboxed objects cannot take advantage of literal notation, which can make for clunky parameter passing and variable declarations.

Wrap Up

JavaScript is a prototypical language — adding a definition to the prototype makes it immediately available to all instances — and the prototypes of its core objects are well documented and freely available for extension. Moreover everything in JavaScript is an instance and when we are forced (jQuery-like) to wrap our utilities in static wrappers it plays against the language, trapping our utilities within unintuitive, inverted signatures.

Not augmenting native prototypes can sometimes feel like looking a gift horse in the mouth, or as@andrewdupont lead developer of Prototype.js puts it “leaving the plastic on the couch”. Yes, there are compelling reasons to be wary and precautions to take, but there are also situations where its safe, and beneficial to rip away that plastic.

It’s quite possible that you are working in a small team, or on your own, with full control over the programming environment and the ability to change course at short notice. Or maybe your project does not require cross-browser support. Or perhaps (dare I say it) the average development team is just a little more diligent than the fearmongers would credit. String.prototype.trim was a trouble-free extension in many developer codebases long before it made its way into the ES 5 specification, at which point it was fairly easy to add a guard to delegate to native versions where available. And we have short memories. Prototype.js and Mootools did not break the web; far from it. Many great JavaScript projects were built on the shoulders of these frameworks and Prototype’s pioneering extensions created the cow paths which ES 5 subsequently paved to the benefit of the entire community.

A word about dogma. Far too many JavaScript how-tos and style guides proclaim (with miraculous certainty) that augmenting native prototypes is an unspeakable evil, while offering little or nothing in the way of substantive evidence (beyond alarmist warnings about breaking for in loops which in reality were only ever relevant to that relic of bygone age known asObject.prototype.myNuttyExtension). We shouldn’t ask people to follow rules that we can’t explain or propose actions that we can’t defend.

Native extensions are neither right or wrong; as with so much in the JavaScript realm, there’s more grey than black-and-white. The best we can do is get informed and weigh each case on its merits. Be thoroughly aware of the consequences, play well with others, but whenever it makes sense, make the language do the work for you.

Extending JavaScript Natives的更多相关文章

  1. Dreamweaver 扩展开发: Calling a C++ function from JavaScript

    After you understand how C-level extensibility works in Dreamweaver and its dependency on certain da ...

  2. 【转】Native JavaScript Data-Binding

    原文转自:http://www.sellarafaeli.com/blog/native_javascript_data_binding Two-way data-binding is such an ...

  3. Javascript.ReactNative-2-javascript-syntax-in-react-native

    JavaScript Syntax in React Native Contents: Arrow Function Let+Const Default + Rest + Spread Destruc ...

  4. 15款增强web体验的Javascript库

    1. Pikaday: Standalone JavaScript Datepicker 这是一个令人耳目一新的JavaScript日期选择器 轻量轻(压缩和gzip后小于5KB) 没有依赖其它JS框 ...

  5. 【javascript】html5中使用canvas编写头像上传截取功能

    [javascript]html5中使用canvas编写头像上传截取功能 本人对canvas很是喜欢,于是想仿照新浪微博头像上传功能(前端使用canvas) 本程序目前在谷歌浏览器和火狐浏览器测试可用 ...

  6. 使用AmplifyJS和JQuery编写更好更优雅的javascript事件处理代码

    事件(或消息)是一种经常使用的软件设计模式.可以减少消息处理者和消息公布者的之间的耦合,比方J2EE里面的JMS规范.设计模式中的观察者模式(也叫公布/订阅模式).这对于javascript代码相同适 ...

  7. JavaScript Garden

    Objects Object Usage and Properties Everything in JavaScript acts like an object, with the only two ...

  8. JavaScript Application Architecture On The Road To 2015

    JavaScript Application Architecture On The Road To 2015 I once told someone I was an architect. It’s ...

  9. JavaScript: Class.method vs Class.prototype.method

    在stack overflow中看到一个人回答,如下   // constructor function function MyClass () { var privateVariable; // p ...

随机推荐

  1. Spring 数据源配置一:单一数据源

    最近遇到一个项目,需要访问都多个数据源,并且数据库是不同厂商(mysql,  sqlserver). 所以对此做了一些研究,这里咱们采用渐进的方式来展开,先谈谈单一数据源配置.(稍后有时间会陆续补充其 ...

  2. join的一对多,去除重复,排序优先的group方法

    想将问题列表按照最新回答来排列.但问题和回答是分拆在两张表来存放的.所以,要完成上述需求,需从主表“问题”取显示数据,但是得按照次表(回答)的更新日期来排序. 用join来做,始终无法去除重复,折腾了 ...

  3. POJ2201+RMQ

    /* RMQ */ #include<stdio.h> #include<string.h> #include<stdlib.h> #include<algo ...

  4. 李洪强漫谈iOS开发[C语言-019]-断点调试

  5. DHTMLX 前端框架 建立你的一个应用程序 教程(十一)--添加/删除表格中的记录

    添加/删除表格中的记录 我们的最终功能是在表格中添加删除 我们通过单机工具栏上的按钮来实现添加删除 当我们单击添加按钮的时候, 表单中 第一行默认填写New contact 光标自动聚焦 当用户点击删 ...

  6. OpenSSH for Windows,CopSSH

    https://www.oschina.net/p/openssh+for+windows https://www.oschina.net/p/copssh

  7. SQL Server中时间段查询

    /****** Script for SelectTopNRows command from SSMS ******/ select * from dbo.VehicleData20100901 wh ...

  8. is present but cannot be translated into a null value due to being declared as a primitive type

    解决办法:把基本类型改为对象,譬如此处将pageId的类型由int 改为Integer 2016-10-19 19:36:11.275 DEBUG [http-nio-9999-exec-2][org ...

  9. 转载:简化IT程序员工作生活的4个窍门

    如果可以简化你的生活——少做枯燥的任务,将时间真正地用于完成事情,你愿不愿意去尝试?下面就让我一起来学一下如何让程序员工作生活变得简单的小窍门.如果你敢于倾听自己的心声,你会发现自己一天中的大多数时间 ...

  10. MySQL导入数据非常慢的解决办法

    MySQL导出的SQL语句在导入时有可能会非常非常慢,经历过导入仅45万条记录,竟用了近3个小时.在导出时合理使用几个参数,可以大大加快导入的速度. -e 使用包括几个VALUES列表的多行INSER ...