Extending JavaScript Natives

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

//(results will vary by browser)

Object.getOwnPropertyNames(Function.prototype)
//["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:

//create a new array method that removes a member
Array.prototype.remove = function(member) {
  var index = this.indexOf(member);
  if (index > -1) {
    this.splice(index, 1);
  }
  return this;
}

['poppy', 'sesame', 'plain'].remove('poppy'); //["sesame", "plain"]
['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.

var myObj = {};
var A = function() {};
var ABound = A.bind(myObj);

(new ABound()) instanceof A;
//true (in browsers which faithfully implement ES5 bind)
//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:

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

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:

Object.prototype.values = function() {
  //etc..
};

//later..
var competitors = [];
var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
for (var prop in results) {
  competitors[competitors.length] = prop;
}

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.

var competitors = [];
var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
for (var prop in results) {
  results.hasOwnProperty(prop) && competitors.push(prop);
}

competitors; //["Mary", "Ana", "Evelyn"]

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

//supporting browsers only (not IE version 8 and earlier)
Object.defineProperty(
  Object.prototype, 'values', {enumerable: false});

var competitors = [];
var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
for (var prop in results) {
  competitors[competitors.length] = prop;
}

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:

Object.prototype.archive = function() {
  //etc..
}

var concerto = {
  composer: 'Mozart',
  archive: 'K. 488'
}

concerto.archive();
//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):

//see https://github.com/kriskowal/es5-shim

if (!Array.prototype.forEach) {
  Array.prototype.forEach = function forEach(fun /*, thisp*/) {
    var self = toObject(this),
        thisp = arguments[1],
        i = -1,
        length = self.length >>> 0;

    if (_toString(fun) != '[object Function]') {
      throw new TypeError(); // TODO message
    }

    while (++i < length) {
      if (i in self) {
        fun.call(thisp, self[i], i, self);
      }
    }
  };
}

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 as Function.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:

//Rough and ready version to illustrate technique
//For production-ready version see http://msdn.microsoft.com/en-us/scriptjunkie/gg278167
var sb, iframe = document.createElement('IFRAME');
document.body.appendChild(iframe);
sb = window.frames[1];

//later...
sb.Array.prototype.remove = function(member) {
  var index = this.indexOf(member);
  if (index > -1) {
    this.splice(index, 1);
  }
  return this;
}

//much later...
var arr = new sb.Array('carrot', 'potato', 'leek');
arr.remove('potato');
arr; //['carrot', 'leek']

//global array is untouched
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 as Object.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.

Additional Resources

Andrew Dupont: Everything is Permitted (JSConf video presentation)
Juriy Zaytsev (Kangax): What’s wrong with extending the DOM
Juriy Zaytsev (Kangax): Extending Built-in Native Objects, Evil or Not
John David Dalton: Sandboxed Natives: Have Your Cake and Eat It, Too
Kris Kowal: ES 5 Shim
Eric Arvidsson: Object.prototype is verboten

54 thoughts on “Extending JavaScript Natives

  1. I’m totally after fixing broken implementations, so using *well implemented* shims in my opinion should be encouraged.

    in case of custom, not standard (and not planned to be standard) extensions I recently came up with following approach:
    Any generic tool or library that is not aware about context in which will be used should never extend native objects and should never rely on there might be custom extensions on native objects.
    However each application has it’s application related logic, I would call it a master code, I think on this level where we definitely *own* the context it’s ok to extend native objects, and still safely use external extensions that won’t have issues with that.
    It’s something like master/slave concept. There can be only one master and many slaves.
    Master code is allowed to do so, slaves should never touch it.

  2. There is absolutely nothing wrong with extending natives if that is what _you_ want to do.

    There is a big problem with frameworks or general purpose libraries _assuming_ that you want to extend natives.

    Extending natives is a decision a library user should make, not a decision a library author should make.

  3. Nice write-up, as usual!

    Note that code like Array.prototype.remove = Array.prototype.remove || fn; will make Array.prototype.remove enumerable even if it’s natively available.

    1. Great point!

      This can be fixed (as you know) by doing something like this…
      Array.prototype.remove || (Array.prototype.remove = fn)

      Wonder if thats desirable in this case though – maybe its better to have a method be either enumerable or non-enumerable across browsers
      What do you think?

  4. Great article.

    I wonder if some of the concern about extending natives comes from a confusion of natives and host objects. I’ll admit that at one point I was fuzzy on the distinction, so any talk of the danger of extending host objects tended to bleed over into my understanding of extending natives.

    I wonder if it’s worth including a note on *why* you need to borrow an Array constructor from an iframe rather than just subclassing Array – Kangax seems to have the definitive description of this as usual 🙂 http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/

  5. I have a few comments.

    As Mathias mentioned there could be unexpected enumeration issues using this pattern Array.prototype.remove = [].remove || function(){ .. } as well as other neat bugs/features.

    http://webk.it/66451
    http://code.google.com/p/v8/issues/detail?id=1623

    The current stable 1.7 and edge repo version of Prototype pave existing native methods for Array#every, Array#filter, Array#map, Array#reverse, Array#some, Function#bind, implement incorrect shims for the previous listed as well as Array#indexOf and Array#lastIndexOf, and produce inconsistent cross-browser functionality for String#strip (which leverages String#trim when available). Previous Prototype versions also paved Array#reduce with a non-standard implementation.

    While Prototype.js and MooTools did not break the web in a doomsday sense, various versions have broken native JSON (among other things) on pages which, still today, cause headaches and lost dev time for other libs/third-party code.

    In your “extending Object.prototype” section you should mention that extending Object.prototype can and does cause bugs/errors in libs/frameworks.

    On ES5 shims you wrote that shims must either fail silently or throw an exception. I would argue that neither is acceptable. If the primary functionality (ignoring edge cases like those associated with Function#bind) of a method cannot be reproduced then the shim *should not* be created. Instead devs can break the partially implemented chunks into separate private utility methods as needed.

    https://github.com/kriskowal/es5-shim/pull/84

    You also mentioned that libs like Underscore.js lock devs into non-standard, inverted signatures, but Underscore.js can be used as a wrapper; _([1, 2, 3, 4]).chain().each(...).map(...) which gives a more “native” feel through chaining.

    http://documentcloud.github.com/underscore/#chain

    I also wanted to respond to your comment about uncertainties with sandboxed natives.
    I built a framework around them and didn’t have problems knowing if something was a sandboxed native or not because when I needed sandboxed functionality I used the sandboxed native’s methods generically much like many do with normal natives [].slice.call(arguments, 1). Also I don’t mind that sandboxed natives lack primitive syntax, because it helps more easily identify them in code. Aside from their declarations, sandboxed natives have the correct internal [[Class]] value and chain just like normal natives. That said, I am totally excited for something like the proposed scoped object extensions.

    http://wiki.ecmascript.org/doku.php?id=strawman:scoped_object_extensions

    Back in August I posted a gist of common ES5 shim mistakes made by popular libs/frameworks. To their credit many of these projects have addressed the issues I posted on.

    https://gist.github.com/1120592

    However, libs and devs need to stay on top of it. For example, several of Twitter’s widgets pave existing native Array#filter and Array#indexOf with non-standard implementations.

    https://twitter.com/about/resources/widgets

    Your last paragraph is right on 😀

  6. Forwards compatibility has got to be the most important reason for me, since everything else can be tested for before release.
    Thanks for the informative article!

  7. Awesome article… agree with every word. I’ve been working on Sugar (sugarjs.com) and it completely echoes your sentiments. By not extending natives we are shooting ourselves in the foot, but doing it in conscientious way is when things really get interesting:

    1. Avoid Object.prototype
    2. Avoid host objects altogether
    3. Mitigate for..in issues with non-enumerable properties.
    4. Always bail when methods are defined.
    5. Shim and shim WELL (I actually owe @jdalton above a beer for this one)
    6. Don’t shim if you can’t shim 100% (Object.create, Object.isSealed, Object.isFrozen, etc.)
    7. Unit test like there’s no tomorrow.
    8. Ignore the naysayers.

    Libs like Sugar concern themselves *only* with augmenting natives, and I think it’s reasonable to expect you assign only one lib this role. In exchange it should take on the hard and murky issues in this area like future-proofing and playing nicely with other libs… all so that you don’t have to worry about it.

  8. @Daniel15, @Michal, @Peter, @Mathias, @skilldrick, @codylindley, @Jonathan, @Andrew

    Thanks, glad you liked it!

    @Maiusz

    Yep that approach is sensible and doesn’t clip too many wings

    @Raynos

    Many users load a library with no clue as to how it works. I think its ok for a library to extend natives if it does it openly and sensibly.

    @skilldrick

    Yes. That’s why I put in a paragraph about hosts in case folks thought I had been talking about them all along. I actually think most of the concern is assuming we are extending either Object.prototype or hosts – even though hardly anyone does that kind of extension. It’s like saying never cross a road because you know freeways are really dangerous 🙂

    Considered writing about array subclass but it was already getting late. Plus as you say @kangax covers that 100%

    @jdalton

    Lots of valid points, especially about shim mistakes by popular frameworks. Thanks.

    @jonathan

    agree 100%

    @andrew

    Going to go back and look at sugar.js some more (I looked at it briefly when it first came out). Nicely put – I may disagree with point 6 though – really no-one can shim 100% of the ES features – I think there’s a good argument for taking what we can get and knowing what we’re missing (or did you mean apply the 100% standard to each method separately? In which case I’d still take an Object.prototype.bind that does 99% of what I expect it to, as long as the missing 1% did not compromise my code)

  9. Yes, poorly worded. I meant each method separately and do agree that 99% is OK. I was essentially agreeing with @jdalton above: “If the primary functionality (ignoring edge cases like those associated with Function#bind) of a method cannot be reproduced then the shim *should not* be created.”.

    This is perhaps a gray area theoretically, but it all boils down to a small set of internals that are either exposed or not, so fortunately in practice it’s pretty clear-cut. Many libs shim early FF/Webkit versions but fail silently on IE. This isn’t a practical solution and will complicate debugging in IE, so I say avoid it altogether… again, same exact point @jdalton made.

  10. Regarding future-proofing is there a convention for vendor extensions to the language? It seems like the ES committee could just say they’ll never create a new method or property that starts with say underscore (_). That gives developers a sandbox to play in.

  11. If you were actually trying to do something like this for real code, I wouldn’t bother defining things in the Collection.prototype–I’d just immediately extend the Array object you create in the Collection() constructor.

    JVM Host

  12. This is a little nuts. You’re suggesting to not leverage the design of the language because bad code might break in the future.

    Bad code always breaks in the future, and even when you go backwards (in browser versions), or sideways to untested Clients.

    Prototype augmentation is valid, therefore it should be embraced. It’s an OOP paradigm.

    1. I’m in total agreement with you. Not extending the native classes is a bit like being told to embrace C++ or Java but to “avoid all that class stuff–it’ll just get you into trouble.”

      (Multiple-inheritance in C++, yes, bad idea–but otherwise….)

  13. Why there is criticism, can’t we be future proof with simple construct like this?

    Array.prototype.remove = Array.prototype.remove || function () { … }

    Anything wrong with above ?

  14. The argument about whether or not it’s okay to extend JavaScript natives tends to focus too much on whether you should or shouldn’t, instead of on when you should and shouldn’t, which would be a much more useful discussion.

    UK VPN

  15. Just wanted to say to Angus that you’ve been a reassuring voice during my quest to write VerbotenJS, my object prototype extension framework. I’m a huge fan of prototypal inheritance and believe that a framework is the best place to extend both native objects and host objects because it’s the best option for standardizing the functionality across developer circles.

    I even think extending host objects is safe to do these days. I couldn’t quite finish a dom wrapper I was writing that takes one of three strategies for modifying host objects with either wrappers or sublcasses. The result is the same interface to your dom elements, with normalized functions that protect against cross-browser inconsistencies, along with massive efficiencies in memory consumption with very little performance loss. Sure, it’s not future proof, but then again no library is future proof. Since I couldn’t finish dom.js I just forked a version of jQuery for now.

    If you ever get some free time to check out the project I’d love to hear some feedback from you.
    http://www.verbotenjs.com/

  16. Great Article, i found about native extensions a while ago, but i thought about the compatibility problems with third party js programs or new browsers extensions. You helped me to understand how to deal with this functionality, thank you

  17. I just wanted to leave a point saying that I’m just now wrapping up the most ambitious project I’ve ever developed in my career, a project I’ve worked on for over a year now. The project was purely based around, from javascript, doing real time analytics of 3rd party technology in the browser, and bypassing obfuscation in various industries (ad technology, tracking, privacy, malware, etc), to figure out who is doing what, what, on the web.

    Anyway, the _only_ way the project was even remotely possible, is because both IE and FF allow _BOTH_ the ability to overwrite / customize getter and setters of native HTML dom elements, AND hold an instance of the original getter / setter function in memory for reassignment.

    These are things that are not currently available in webkit (though are currently entering their 6th year, and 3rd attempt at implementation) or most mobile browsers. But without it, it becomes _completely_ possible to force the running of external js without the ability for the user to even detect its existence or purpose.

    So needless to say, I’m happy that we’re actually including this functionality into browsers, and hope we can keep discussing manners of implementing it in a safe and responsible way.

Leave a reply to Mathias Bynens Cancel reply