Extending JavaScript natives always causes debate with several developers bringing out the pitfalls that one could encounter while doing this. In this blog, we explore the primary areas of contention against this, and the subtleties of doing it correctly.
JavaScript types are typically constructors whose prototypes contain the methods (and properties) that define their default behavior:
--
//(results vary based on browser)
Object.getOwnPropertyNames(Function.prototype)
//["bind", "arguments", "length", "call", "name", "apply",
"constructor"]
--
Of course, you can't delete or replace a native prototype, but one can edit the values of its properties, or create new ones as below:
--
|
//creating anarray method that removes a member |
2 |
Array.prototype.removeMember = function(member) { |
3 |
varindex = this.indexOf(member); |
4 |
if(index > -1) { |
5 |
this.splice(index, 1); |
6 |
} |
7 |
returnthis; |
8 |
} |
9 |
|
10 |
['CSS','jQuery','bootstrap'].removeMember('CSS'); //["jQuery", "boot |
11 |
|
--
As demonstrated, our code gets a helpful array extension for free. However as mentioned earlier, several developers would disapprove of this for different reasons. We’ll focus on some of the more important concerns raised:
Common Challenges
1. Future-proofing
If future browser versions (or mobile or tablet versions of browsers) implement Array.prototype.removeMember, their implementation will be overridden by our custom one. This could obviously result in a different, non standard outcome.
E.g. The Prototype.js framework implemented Function.prototype.bind. Several years later, the Ecma-262 committee includedFunction.prototype.bind in their ES 5 specification. Unfortunately for Prototype.js users, the brand new ES 5 standard required additional functionality, that was not supported by the elegantly simple Prototype.js version.
Similarly, software that utilizes third-party libraries run the danger that the native prototype augmentation (home grown) could possibly be clobbered by an alternate implementation of exactly the same property by another library.
These concerns may be partially mitigated by checking for the existence of a native property before implementing it:
Array.prototype.removeMember = Array.prototype.removeMember
|| function(member)
{
varindex = this.indexOf(member);
if(index > -1) {
this.splice(index, 1);
}
returnthis;
}
This solution is dependent upon simultaneous adoption of new functionality across browsers. If a particular browser implemented Array.prototype.removeMember first, then other browsers would still fall back on the home grown implementation which could do something entirely different. For this reason Prototype.js would have a problem with this strategy: since Array.prototype.bind is not implemented in some older browsers, those browsers would fall back on Prototype.js's more limited functionality.
2. The ‘for in’ loop
Another common concern is that extending natives messes with the object iteration cycle. Essentially, since ‘for in’ loops will visit all enumerable properties in the object's prototype chain, custom native properties will unexpectedly be part of such iterations.
Object.prototype.values = function()
{
//function specifics
}
varcompetitors = [];
varresults = {'Pegasus':'1','Accentyre':'2', 'IBMi':'3'};
for(varprop inresults) {
competitors[competitors.length] = prop;
}
competitors; //["Pegasus", "Accentyre", "IBMi", “values”]!
However, in such cases, the hasOwnProperty method can be used to filter inherited properties.
varcompetitors = [];
varresults = {'Pegasus':'1','Accentyre':'2', 'IBMi':'3'};
for(varprop inresults) {
results.hasOwnProperty(prop) && competitors.push(prop);
}
competitors; //["Pegasus", "Accentyre", "IBMi”]
Alternately, ES 5 allows properties to be designated as “non-enumerable”, which can be used to make them immune from for in iteration:
3 |
//does not support first generation browsers |
4 |
Object.defineProperty( |
5 |
Object.prototype, 'values', {enumerable: false}); |
6 |
|
7 |
varcompetitors = []; |
8 |
varresults = {'Pegasus':'1','Accentyre':'2', 'IBMi':'3'}; |
9 |
for(varprop inresults) { |
10 |
competitors[competitors.length] = prop; |
11 |
} |
|
|
|
competitors; //["Pegasus", "Accentyre", "IBMi”] |
3. Shadowing
With regards to extending Object.prototype (as against native objects in general) there's another reason to be wary. Descendants of Object.prototype will miss usage of the extended property if another property gets defined with exactly the same name. So each time we define a property on Object.prototype, we are effectively generating a new reserved term. So Object.prototype extensions are not recommended by senior developers.
Correct Implementation
To extend natives, one must plan for each of the above concerns, and decide whether the extension would add power and clarity to the codebase.
Code Shims
Code shims present an excellent case for extending natives. A shim is a piece of code built to reconcile differences across environments, by supplying missing implementations. ES 5 support is patchy in older browsers, which is often frustrating for developers who would like to make the most of the newest ES 5 features, in addition to supporting older browsers. Here's a popular shim that demonstrates how this can be used:
1 |
if(!Array.prototype.forEach) { |
2 |
Array.prototype.forEach = functionforEach(fun /*, thisp*/) { |
3 |
varself = toObject(this), |
4 |
thisp = arguments[1], |
5 |
i = -1, |
6 |
length = self.length >>> 0; |
7 |
|
8 |
if(_toString(fun) != '[object Function]') { |
9 |
thrownewTypeError(); |
10 |
} |
11 |
|
12 |
while(++i < length) { |
13 |
if(i inself) { |
14 |
fun.call(thisp, self[i], i, self); |
15 |
} |
16 |
} |
17 |
}; |
18 |
|
19 |
|
20 |
|
The initial statement checks if Array.prototype.forEach is implemented and bails if it is. All properties included with native prototypes are defined by the ES 5 standard so its safe to assume they'll not collide with unrelated namesake properties later; no ES 5 property extends Object.prototype so pollution of for in enumerations will not occur; every ES 5 property is well documented so there's no basis for ambiguity as to how the shim must be implemented and it's clear which names are effectively reserved by the ES 5 standard.
Shimming ES 5 extensions makes plenty of sense. Without them we're hostage to the inadequacies of lesser browsers and unable to make the most of the language's standard utility set. Yes, we could utilize the same functionality offered by well-built libraries like underscore.js, but nonetheless we're locked into non-standard, inverted signatures where methods are static and objects are merely extra arguments.
Sandboxing
By Sandboxing, we are able to have our personal private array, string or function object that we could extend and use as required, without complicating the global version. There are many approaches for creating sandboxed natives, but the most browser-neutral one uses an IFRAME:
1 |
//Building a rough version as a demo |
2 |
|
3 |
varsb, iframe = document.createElement('IFRAME'); |
4 |
document.body.appendChild(iframe); |
5 |
sb = window.frames[1]; |
6 |
|
7 |
//later in code base... |
8 |
sb.Array.prototype.removeMember = function(member) { |
9 |
varindex = this.indexOf(member); |
10 |
if(index > -1) { |
11 |
this.splice(index, 1); |
12 |
} |
13 |
returnthis; |
14 |
} |
15 |
|
16 |
//further ahead in code base... |
17 |
vararr = newsb.Array('Pegasus', 'Accentyre', 'IBMi'); |
18 |
arr.removeMember(‘IMBi’); |
19 |
arr; //['Pegasus', 'Accentyre'] |
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 good compromise but a compromise just the same. After all the power of prototoype extensions is in their ability to change all instances of a certain type and provide all of them with access to exactly the same behavior set. With sandboxing we are required to understand which of our array instances are “super-arrays” and which are native.
Conclusion
Not augmenting native prototypes is like keeping your old car untouched, and never opening the hood yourself. Yes, there are precautions to be taken when working on this, but there are situations where its safe, and advantageous to do the required modifications.
Prototype.js and Mootools didn't break the Internet. Many great JavaScript projects were built on the shoulders on these frameworks and Prototype's pioneering extensions created the paths which ES 5 subsequently paved to the advantage the whole community.
Native extensions are neither right or wrong; there's more grey than black-and-white. The most effective way do is to make informed decisions and weigh each case on its merits.
|