Okay, listen now:
You should never, ever, under any circumstances extend Object.prototype with new members unless you own all the code in the entire production environment.
The reason is simple: Doing so breaks the in operator on all types of objects used in JavaScript. This includes the for(i in o) and for each(i in o) iterators, and can and will break other people’s code. So, don’t even think of doing it. It’s absolutely forbidden, understand?
You should avoid, if at all possible, extending Array.prototype with new members.
The reason: Even though Array objects only have special behavior on positive integer indices, they have one distinct property* that arrays in other languages with more static typing don’t have: JavaScript arrays are sparse. What does that mean? Well, it means that in JavaScript, an array of length 4 might have the properties 0, 1, 2, 3. However, it also might have only the property 3, with the rest undefined. In fact, it doesn’t even have to have a single property defined, if somebody has set it’s length property to 4 manually.
Why is this important, then? Well, sure, the overwhelming number of uses of arrays will be dense. And for these uses, using a normal for(i=0,l=a.length;0>l;i++) loop is the recommended way of iteration. However, if the array is sparse, the only way to reliably iterate over it is the for(i in a) syntax, and that is broken by adding properties to the prototype.
So, do people actually use JavaScript arrays this way? The answer is yes, because I’ve seen some people asking for help because of this in conjunction with use of a certain very popular library… A prime example is the code of a prime finder, that filled in only those properties that were verified prime numbers. Most people think twice after having been hit by it the first time though, and start using a check for a.hasOwnProperty(i) in the loop or find another solution than sparse arrays.
But really, they shouldn’t have to do that. You see, extending Array.prototype is only a way for you to offload the type checking to everybody else. Another solution is that you extend all arrays you use like this:
function mkCustomArray(arr){
if(!(arr instanceof Array))
arr=[];
if(!arr.isCustomArray){
arr.customMember=value;
arr.isCustomArray=true;
}
return arr;
}
Calling this function on all arrays you yourself manipulate will turn them into your “custom array”, by changing the individual instance instead of it’s prototype. That means you only break others’s code if they happen to give you an array that they later on use a for(i in a) iterator on, something that you can correct by deleting all members you added on the array after finishing with it.
As for the other prototypes, they are not at all as problematic as these two. Why? Because Number, Boolean and String are almost always used as primitives and not objects. Their members are very seldom iterated over since primitives cannot have custom properties. So, how about Function and RegExp that aren’t primitives? Well, these are generally not a problem since they seldom are used contain custom properties, and even more seldom are iterated over.
In general, Array objects should be used for their array-like behavior. If all you want to get is a way to map keys to values, use an Object object instead. If you really want array-like behavior though, by all means use them. It shouldn’t be your responsibility to keep track of whether other people extend prototypes, so you should be able to rely on for(i in a) as a way to iterate over sparse arrays.
Also, to address a specific point made by Chris Double:
Unfortunately using ‘Object’ has its own set of pitfalls. Try the following code in your favourite Javascript implementation:
var x = {} x["toString"]This will display details on the toString method on ‘Object’. So we can’t use a plan Object as an associative array as it has some default entries in it already obtained from its prototype.
Now, this trouble will exist for all types of objects. But it’s not that much of a trouble really. First of all, built-in members of prototypes are not enumerable, and thus you can’t find the key programmatically. Second, if you want to know if a key exists as a direct member of the object and not inherited from the prototype chain, you can query it for that. A query function, I might add, that in turn is such an inherited member.
This post was in response to a series of posts I’ve seen on blogs lately, such as Bluish Coder: Associative Arrays and Javascript (Chris Double), JavaScript “Associative Arraysâ€? Considered Harmful (Andrew Dupont), Misuse of the Array Object in JavaScript (Alexander Kirk) and Object.prototype is verboten (Erik Arvidsson).
* Actually they have many distinct properties from, say, VBScript arrays.
They have string keys and not integer indices, though they have special handling for keys that can be converted to uint32.
They can’t be multi dimentional.
They are not type restricted.
They are sparse and not dense.
They are not fixed sized, and occupy just as much space as needed for all their members.
Posted in Javascript |
You can follow any responses to this entry through the RSS 2.0 feed. You can skip to the end and leave a response. Pinging is currently not allowed.
20 Responses to “On modifying prototypes of JavaScript built-ins”
May 23rd, 2006 at 9:35 am
Good post. I agree that the issue with retrieving built in properties from the prototype is not a big issue. It is an issue in the specific case of using an object as a general hashtable where any string key can be stored, retrieved and queries from unknown input. Which is the case in code I’ve inherited, and has been broken due to not using hasOwnProperty, or overwriting methods stored in the object. It basically boils down to ‘understand what you are doing, why you are doing it, and the effect that it will have, before you do it’.
May 23rd, 2006 at 10:46 am
I’m going to assume your talking about the Prototype library, not really sure why people avoid calling it by it’s name. If I’m wrong, forgive me.
I would say no to modifying Array.prototype, if and only if you didn’t also provide a replacement for iteration mechanisms like for..in. Prototype however provides an iteration mechanism which pretty much (I haven’t found a case where I need for..in for Arrays) eliminates the need for for..in in conjunction with Arrays.
Touching on the sparse arrays, this works fine in Prototype:
var foo = new Array();
foo[3] = ‘bar’;
foo[7] = ‘more bar’;
console.log(foo.without(undefined));
console.log(foo.findAll(function(item) {
return item != undefined;
}));
Both of these get you back all the items that are not undefined.
I agree that if you modify the prototype of Array it has a risk of breaking other libraries, but shouldn’t this be left up to the person using the libraries to determine if they’re compatible? I could argue that the hood of my wife’s car, won’t bolt on to mine, even though both cars share a common purpose.
I feel that if you use something created by someone else you have to subscribe yourself to a set of principles set forth by the creator. You have to learn the conventions. Prototype is popular for a reason, admittedly some of this popularity is due to the fact it’s bundled with Rails, but it’s reach goes way beyond that.
- Cheers
May 23rd, 2006 at 10:47 am
Points well taken.
But as long as we are forbidding things, I would much rather discourage the use of for/in than get rid of the über-handy Array enxtensions! Lord knows I have written my share of code that extends Array, and am just trying to assuage my guilt. :-)
May 23rd, 2006 at 11:50 am
I am relatively new to serious Javascript programming, and I don’t understand this post completely. If I extend Prototype with some custom function I’ve written for my purposes, will it necessarily interfere with the in operator, or are we just talking about the fact that there are extensions of Prototype that COULD potentially do so?
May 23rd, 2006 at 12:03 pm
Leo,
You are confusing (understandably) the Prototype framework with the
prototypeinheritance mechanism of the JavaScript language itself. Extending Prototype (the framework) is no problem. Entending JavaScript’s built-in objects using theprototypeproperty is the issue here.May 24th, 2006 at 1:30 am
Justin: You’re assuming that the choice of using a library always belongs to the current programmer. Well, what if the programmer is not alone on the project? What if the programmer is taking over the project from another programmer? What if the problem lies in that the programmer can’t combine two libraries without having to rewrite one of them to accomodate the other?
Over all when writing a library, modifying prototypes of built-ins — at least when those built-ins are
ObjectandArray— is something you should avoid, because your library might break other libraries or your users’s code.Also, don’t assume asking for whether a member is
undefinedis the same as asking for whether the member exists. I have written code where you can find tests like these:May 24th, 2006 at 8:19 am
liorean: That doesn’t fall on the creator of a library. It’s up to the working team to evaluate libraries they do or don’t want to use. If Bill and Jane don’t think Prototype or other libraries which modify the .prototype of built ins are suitable for their project, they shouldn’t use it. It won’t be everything to everybody, nor does it pretend to be. It’s like I’ve said before, you either love it or you hate it, but if you want to use it, you have to accept it along with it’s philosophy.
I completely agree with you about Object.prototype, but not Array.prototype. Prototype was *designed* to be an extension of Javascript, not a framework that sits on top of it. It makes Javascript better for 80% of the people that deal with *every day* problems.
If a programmer is taking over a project from another, he/she will always be faced with constraints. I think the scenerio of clashes in Javascript will be the least of his/her worries. That person should be griping at the programmers that used to be on the project for choosing something like Prototype. Prototype doesn’t hide the fact it modifies built-in objects.
On your last point, fair enough. It was a quick example to show it could be done. It could be easily rewritten to do a complete check.
–Cheers
May 25th, 2006 at 3:58 am
Good thoughts. However I’m going to keep my
Array.prototype.inArray(needle). But then again, I am generally the sole person in a project when that gets used.Any thoughts on extending Function.prototype as well? Drawbacks, caveats?
May 25th, 2006 at 12:42 pm
If sparse arrays is the only argument for not modifying Array.prototype at least you have convinced me that it is acceptable.
The only property Array has that Object does not is the magic length property and in the few cases where a sparse array is needed object and manually keeping track of the length seems good enough to me.
Like I said there is nothing except the magical length property that makes an array an array (as well as the array literal syntactic sugar and the prototype). Even the methods on Array.prototype are generic enough to work on any mutable object that looks like an array:
var a = {‘0′: 1, ‘1′: 3, ‘2′: 0, ‘3′: 2, length: 4};
Array.prototype.sort.call(a);
console.log(Array.prototype.toString.call(a));
try changing a to [1,2,,0] as well as to {‘0′: 1, ‘1′: 2, ‘3′: 0, length: 4}.
May 25th, 2006 at 12:51 pm
Oh, one more thing. The reason toString is not showing up in for-in loops and when using the in operator is an JScript bug.
https://connect.microsoft.com/feedback/ViewFeedback.aspx?SiteID=136&FeedbackID=72707
May 25th, 2006 at 5:43 pm
Erik: It should only appear in an enumeration if it was overridden — which is the case that bug is about. But I’m talking about the case where it’s not overridden – in that case, it will not be enumerated, because ECMA-262 3ed. specifies that it’s {DontEnum}. So, there’s no way of autodiscovering it in code. The problem is one of getting the property from the prototype when you in fact only wanted properties from the actual object. And for that, you only need to check whether it exists on the object at hand or not, which can be done using
arr.hasOwnProperty(key).As for sparse arrays not being enough of a concern to avoid extending
Array.prototype— maybe you’re right about that. It’s not a common use of arrays. I’m still of the opinion that libraries should try not to destroy native language functionality though.It’s not as if arrays and iterating over them is homogenously implemented anyways. Try this in moz, iew and op:
Moz and iew have their bugs with sparse array literals. Op has a different — but entirely correct accoring to the spec — implementation of the iteration through array members. Or rather of how those members are stored, but it amounts to a difference in the iteration.
May 30th, 2006 at 5:06 pm
I do not understand you problem with extending Object.prototype. You can still iterate over its members as you did before by ignoring the members of Object.prototype.
for (member in object)
{
if (Object.prototype[member] === undefined)
{
//do your thing
}
}
May 30th, 2006 at 5:36 pm
Hermann: Yes, you can. But you’re repeating the error I talked about with regard to the Array prototype, but in larger scale – by extending
Object.prototypeyou are forcing everybody else to do that runtime checking for every object whose members they want to iterate through. You’re breaking all code that didn’t do that checking because they knew that all built-in members are marked {DontEnum}. Breaking language built-ins is bad, because you’re breaking code that normally would work perfectly fine. Any solution to the problem that would demand changing other code is unacceptable.May 31st, 2006 at 4:18 pm
I see your point. Thanks for the clarification!
June 8th, 2006 at 4:57 pm
Liorean, this is a good post, and I wish I had happened upon it sooner. I started to post a comment, but it turned into a theological tract, so I’ll just link it here. Cheers.
June 9th, 2006 at 7:04 am
I really disagree with this post. It is arguments like this that hold back JS development. None of the reasons you come up with have convinced me that extending the Array object is a bad idea.
In one of your code samples you use the example of Array.push. IE5.0 does not support this. How do we get our code to work with IE5.0? We extend the Array object of course.
June 12th, 2006 at 10:50 am
You wrote:
“using a normal for(i=0,l=a.length;0>l;i++) loop”
Shouldn’t this be
for(i=0,l=a.length;l>i;i++)
??
June 15th, 2006 at 10:54 pm
I really disagree too. Prototypes are an important part of JavaScript. It’s not the programmer who extends them that is sloppy – it’s the programmer who won’t check if they’re extended. It is really easy to check this:
//let’s extend another prototype
String.prototype.proto=function(obj){
var proto=obj.sort?Array.prototype:Object.prototype;
return proto[this] && proto[this]==obj[this]
};
// and extend some more prototypes
Object.prototype.you=1;
Array.prototype._dont=1;
Object.prototype.like=1;
Array.prototype._this=1;
// and create a new array
var x=[1,1,1];
x.you=2;
// now it is really easy to iterate through non-prototype members
// this will return 0,1,2,you
props=[];
for(var i in x){
if(!i.proto(x)){
props.push(i)
}
};
alert(props.join(“\n”))
June 15th, 2006 at 10:57 pm
Sorry didn’t get my pre tags right ;-):
//let's extend another prototype
String.prototype.proto=function(obj){
var proto=obj.sort?Array.prototype:Object.prototype;
return proto[this] && proto[this]==obj[this]
};
// and extend some more prototypes
Object.prototype.you=1;
Array.prototype._dont=1;
Object.prototype.like=1;
Array.prototype._this=1;
// and create a new array
var x=[1,1,1];
x.you=2;
// now it is really easy to iterate through non-prototype members
// this will return 0,1,2,you
props=[];
for(var i in x){
if(!i.proto(x)){
props.push(i)
}
};
alert(props.join("\n"))
November 18th, 2006 at 8:15 pm
In response to that last post, all I do to skip extended members is as follows (untested, conceptual):
Array.prototype.extender = window.alert; //random reference
var arrArray = ["foo", "bar", "chicken"];
for (var anyIndex in arrArray)
{
//skip extension
if (anyIndex != “extender”) continue;
//…
//do_something…
}
Leave a Reply