May18

JavaScript “Associative Arrays” Considered Harmful

I hesitate to add to the proliferation of “considered harmful” essays, but this is an important point, and it needs a URL, if only to cut down on the amount of typing I have to do.

The Problem

Try the following code on an empty page, one without any JavaScript libraries added:

var associative_array = new Array();
associative_array["one"] = "Lorem";
associative_array["two"] = "Ipsum";
associative_array["three"] = "dolor";
for (i in associative_array) { alert(i) };

You’ll get three sequential alert boxes: “one”; “two”; “three.” This code has a predictable output and looks logically sound: you’re declaring a new array, giving it three string keys, then iterating over them.

Now do this: replace “Array” with “RegExp” and run the code again. As if by magic, this also works! It’s not the only one. Try Boolean, or Date, or String, and you’ll find they all work as well. It works because all you’re doing is setting properties on an object (in JS, foo["bar"] is the same as foo.bar), and a for..in loop simply iterates over an object’s properties. All data types in JS are objects (or have object representations), so all of them can have arbitrary properties set.

In JavaScript, one really ought to use Object for a set of key/value pairs. But because Array works as demonstrated above, JavaScript arrays (which are meant to be numeric) are often used to hold key/value pairs. This is bad practice. Object should be used instead.

I’m not trying to ridicule or scold. This misconception is too common to attribute it to stupidity, and there are many legitimate reasons for the confusion. But this is something that needs to be cleared up if JavaScript is ever to be used on a grand scale.

If you need further evidence that Array is not meant to be used this way, consider:

  • There is no way to specify string keys in an array constructor.
  • There is no way to specify string keys in an array literal.
  • Array.length does not count them as items. In the above example, associative_array.length will return 0.
  • The page on Arrays in the Mozilla JavaScript reference makes no mention of this usage. (Nor does the ECMAScript specification, by the way, but you’ll have to do your own legwork to verify that, because I’m not linking to page 88 of a kajillion-page PDF.)

The History

This confusion has several other contributing factors:

  • In PHP, a language that many JavaScript users are also familiar with, numeric arrays and associative arrays are treated more or less identically. And since a set of key/value pairs goes by about eleven different names, depending on the language, this usage is quite often a result of unclear definitions of terms.
  • JavaScript is interpreted, not compiled, and many people have learned it by example. Thus a third-party script that uses Array improperly might rub off on its users.
  • JavaScript started with no specification, then received a poor specification, and I know few people who spend their free time reading specifications. Especially bad ones.
  • Because there is no formal construct for key/value pairs, JavaScript cannot distinguish between creating a hash and setting properties on an object. As we’ve demonstrated, any object can have arbitrary properties, and a for..in loop simply iterates over each of these properties, so the code above is not explicitly incorrect.
  • The harmful side effects of using Array for key/value pairs are not experienced unless Array.prototype is extended. Since this is an underutilized feature of JavaScript, it hasn’t been done on a large scale until rather recently.

Why Prototype “breaks” this usage

Concurrent with Prototype’s rise in popularity have been various blog posts complaining that the JavaScript framework “breaks” associative arrays — i.e. Arrays with string keys. It “breaks” them because it adds a handful of useful methods for working with arrays to Array.prototype, and these methods are also iterated over in a for..in loop. This means that when Prototype is included on a page the code above will loop 35 times instead of the original three.

Prototype also extends String with some methods for dealing with strings. If you try to use String as an associative array, your code will loop 20 times instead of three.

I am aware of the mitigating factors — hell, I just enumerated them — but complaining that Prototype “breaks” your ability to use Array as a hash is like complaining that Prototype “breaks” your ability to use String as a hash. It is not Prototype’s fault that JavaScript does not deter this improper use, and it certainly does not mean that Prototype does not “play well with others.” You are free to reject Prototype and keep using Array improperly, but then you give up your right to bitch and moan.

Actually, we’ve been here before: before version 1.4, Prototype added a couple methods onto Object.prototype, meaning that Object couldn’t even be used in the manner I describe, and a bunch of people rightly took Sam Stephenson to task for it. Object.prototype is verboten. Since version 1.4, however, this is no longer an issue, and therefore there is no longer an excuse.

So I will say it again: Array is not meant to be used for key/value pairs. Luckily, there is a dead-simple way to fix this. In the above example, you need only change Array to Object. (Or, if you’re using literal syntax, change [] to {}.) There. Your wrong code is no longer wrong, and it took only a little more work than a simple find-and-replace.

There are plenty of JavaScript frameworks to choose from, and many of them are excellent. I use Prototype because it works for me, and I do not take it personally when other people decide they don’t like it. But I believe Prototype deserves to be hated on its merits, dammit, not because it makes wrong code stop working — especially when the wrong code can be made right in ten seconds.

Comments

  1. Pingback May 18th, 2006
    at 1:31 pm
  2. I think there is a need to point out some more reasons why using for-in loops are bad.

    1. For-in loops on Arrays do not loop over the indeces

    2. For-in loops do not work on array-like objects like NodeLists.

    function printArray(a) { for (var key in a) { print(a[key]); } }

    printArray([0, 1, 2, 3, 4]); // OK

    var a = new Array(10); printArray(a); // Incorrect

    a = document.getElementsByTagName(’*'); printArray(a); // Incorrect

    a = [0, 1, 2, 3, 4]; a.foo = ‘bar’; printArray(a); // Incorrect

    a = new Array; a[3] = 3; printArray(a); // Incorrect

  3. Pingback May 18th, 2006
    at 2:07 pm
  4. Well said that man!

    Prototype’s extensions to the Array object are extremely powerful as are Mozilla’s new Array extras. The only way to support Mozilla’s extensions on legacy browsers is to extend Array.

    BTW, it was me that persuaded Sam to remove the extensions to the Object object from Prototype 1.4. ;-)

  5. @Dean: I was going to give you credit, I promise, but I couldn’t be bothered to find the URL to that discussion thread. :)

    I’m pretty sure Sam would’ve caved in eventually anyway, or else someone would’ve just forked his code and released a “no-downside” version of Prototype. But I’m glad it didn’t come to that.

  6. Just to play devil’s advocat here, but so what if arrays are used like this? Arrays are objects, just like anything else so I don’t see anything wrong with using them however the language allows you to.

    The problem comes from the fact that people expect arrays to always function as arrays instead of realizing that they are objects.

    What, for example, is wrong with:

    var ary = []; ary['value'] = ary.length; ary.push(’value’);

    I do this every once in a while so that I don’t have to scan the entire array to find elements. Given if an item is shifted or popped from the array this breaks, but I’m aware of that and don’t use it in situations where this could happen.

    The only reason anyone has a problem with using Arrays as hashes is because of Prototype. I’m not arguing against prototype here, but assinging a property to an array isn’t wrong simply because Prototype exists.

    Personally I don’t use prototype. I use my own library and I add properties directly onto Array: Array.each(ary, function) instead of ary.each(function). It’s very little extra writing to preserve existing functionality.

  7. @Mark: Good point. I’m not claiming that setting custom properties on an array is always wrong; I’m saying that using Array as a hash is. In your example, you’re using custom properties to augment the way you use an array, and that’s fine, because you’re not iterating over those properties with for..in.

    It is true that you can use the language however you see fit, even to the extent that your code cannot coexist with third-party scripts, but then you’re choosing to be a code hermit, and it’s not a framework’s responsibility to play by your nonstandard rules. In Ruby, for instance, you’re free to redefine the method that adds two numbers together, but I doubt you’d get much sympathy if you wrote a blog post complaining that Rails “broke” your code.

  8. My point is who has the right to say an array can’t be used as a hash? Or that a regex can’t or that a date can’t?

    For example:

    var str = 'this is just a sample string'; var words = /\w+/g; var index = 0;

    while(match = words.exec(str)){ words[match] = index++; }

    for(word in words){ alert(’”‘+word+’” is word #’+(words[word]+1)); }

    Okay, that’s just weird but if someone wanted to do that, why can’t they?

  9. [sorry, can't edit and you apparently can :-)]

    “it’s not a framework’s responsibility to play by your nonstandard rules”

    Array is an extension of Object. Assigning a property to Array is no different than assigning a property to a function or an Object. for(…in…) is not non-standard on an Object so why should it be non-standard on an extension of Object?

  10. @Mark - no one is saying you can’t just that it is better if you don’t.

    Extending native objects is a good way to provide backward compatibility for older browsers (e.g. providing push/pop for IE5.0). If we can’t do this then we are stuck with only the basic features of the language.

    Extending Object.prototype is another matter as then we are left with no object that we can use as a hash.

  11. @Dean - Okay :-)

  12. Hear hear. Good post. Its good to see prototype 1.5 gets this right, and those folks complaining about Array being broken are off base.

  13. Using objects as associative arrays has its own set of problems too. Try:

    var x = {} x["toString"]

    This means you can’t use an object directly as an associative array as the builtin object operations appear as entries. You’ll need to write your associative array object that stores the data in a seperate object and uses ‘hasOwnProperty’ to ensure you don’t get built-ins.

  14. Anyone doing this, using “new Array()” instead of “[]“, or “new Object()” instead of “{}” needs to relearn JavaScript.

  15. @Chris: This is true, but unless you’re accepting arbitrary input and setting it as a property on your object, the chance of collision between your object’s own properties and its builtins is essentially nil. The problem with writing your own hash implementation is that everyone will write his own, and we’ll have 43 different solutions that are incompatible with one another.

    But, yes, I agree that using Object as a hash has its own set of problems. It’s annoying that this is the best we can do, and hopefully Brendan Eich will solve this once and for all in JS2.

  16. That Prototype extends array irritates me to no end. There are two problems with it: you can no longer use sparse arrays, and you can’t write functions which are polymporphic over both collection types without lots of special case code.

    var x = []; x[0] = “zero”; x[2] = “two”;

    How exactly should I iterate over this array if I can’t use for..in?

    function poly(collection){ for(var i in collection){ //do something } }

    How do you write this otherwise?

    The real problem is that for..in is broken. It shouldn’t pick up properties on arrays, or at the very least not prototype properties on them.

  17. @Emmett: Can’t you just declare x as an Object? That seems to work for me in FireBug.

    As for polymorphism, you might want to take your cue from Prototype, even though it may disgust you. Its Enumerable object is inspired by Ruby’s Enumerable mixin. In Ruby, Enumerables can be mixed into any class that implements an each method — just tell it how to iterate over your collection and you get the rest of the methods for free, without having to tell it anything else about your class or doing any code forking. Likewise, in Prototype you need only define an _each method to get the full set of methods that Enumerable provides.

    I definitely agree about for..in, though. The ECMAScript spec describes a {DontEnum} attribute that certain properties have, thereby making them hidden to for..in loops. Unfortunately, this attribute is not exposed to script writers. Again: I hope JS2 fixes this.

  18. Andrew, the changes may be virtually nil unfortunately it happens when you least expect it and can be hard to track down.

    I was working on some code (A Scheme interpreter in Javascript) and was making it work on Firefox. This code kept the environment of Scheme values in an object. One of those was ‘eval’.

    On firefox Object has a builtin ‘eval’ method but not in Explorer. This caused no end of fun when the interpreter was pulling out the native eval function instead of the data structures it was expecting.

    If you look at some of the frameworks out there you’ll see the explosion of hashtable/associative array implementations is already growing which is a shame.

  19. Excellent article!

    I compare the bitching and moaning around Prototype to that which is also around Rails. Although, I agree about the Object.prototype mishap and I’m glad it’s since been corrected.

    You have to learn ‘The Prototype Way’. If you don’t agree with it, don’t use it. Prototype makes a lot of us enjoy spend hours behind the keyboard working with Javascript. I have yet to find something I couldn’t do in Prototype or a case where Prototype made it impossible to do.

    @Dean: Do you think we can talk Sam into converting Class into what Base is now? ;-)

  20. Wait, I’m losing track of all the code constructs that are considered harmful. How about that we all agree that “bad code considered harmful” instead. It’s universally applicable and means we never have to suffer another “considered harmful” pronouncement again.

  21. I’m not trolling, I swear. But…

    While I agree that it’s a bad idea to confuse Javascript Array’s with associative arrays a la PHP/Perl/Ruby, I disagree with the assertion that you should therefore only use numeric keys. You’re basically committing the same crime — on one hand you say “don’t treat them like PHP arrays“, but then you conclude “you should treat them like C arrays instead“. That’s just as bad of a mischaracterization.

    Javascript Arrays are essentially javascript objects with convenience methods thrown in so you can store/maipulate ordinal collections. But that doesn’t mean that you should stop treating them as objects, because they are objects. Javascript was designed around this intent. The point is that you have to know what you’re doing and not rely on assumptions. But of course, this is true with any language.

    So, yeah, don’t treat them like Associative Arrays, but don’t treat them as C arrays either.

    A couple nit-picks (taken from the article and posts):

    “JavaScript arrays (which are meant to be numeric)”

    This is incorrect. In fact, all keys in an Array are strings. If you specify a numeric key javascript simply converts it to a string, and makes it a new property of the object. Try this for example:

    var a1 = new Array(5,6,7); for(key in a1) { alert(typeof key); }

    You’ll note that the all the keys are strings. This is not implementation-specific: this behavior is defined in the ecmascript standard.

    “For-in loops on Arrays do not loop over the indeces”

    Not true. For in loops over all defined properties of an object. For Arrays, this includes numeric and non-numeric keys. I think perhaps the poster meant the other way around, in that you can’t iterate over non-numeric properties in using for(i;<a.length;i++) style.

  22. @Emmett: You could just do function poly(collection){ for(var i=0;i<collection.length;i++){ if (collection[i]==undefined) continue; // do something } } Then again if you’d have x[0] and x[1000] this might not be such a good idea.

  23. Justin Palmer wrote:

    “You have to learn ‘The Prototype Way’. If you don’t agree with it, don’t use it. “

    Would simply inheriting from Array not be the better way? There could be a PrototypeArray (replace this with something short), so we can mix Prototype with other code that uses Array in some (correct or wrong) incompatible way.

    And not using Prototype is getting more and more difficult, since a growing number of libraries are depending on this - mostly great - stuff.

    As Jim Demos pointed out (thanks for your clarifications) there seems to be no “correct way” of using Array. So i think the duty of every libary should be to not touch [Any JS Core Class].prototype ?!

  24. I think this post would have been better titled “For-in considered useless” since it really has no business looping over prototype method extensions.

    It’s not really fair to say that associative arrays are harmful when its the prototype methods causing the pain when using for-ins. I mean, on the same basis, one could consider prototype methods harmful — I wouldn’t, because they are incredibly useful, just like associative arrays.

    Whilst I agree in principle that it makes more sense using Object for key/value pairs, you are not really solving anything long-term. What happens in the future when today’s browsers need to have Object extended with prototype methods. Sure, Sam Stephenson’s prototype.js has abandoned doing this now, but how can we guarantee that frameworks x,y,z won’t need to do something funky to Object in 2008?

    I’d also like to point out that arrays are objects anyway so you can’t really claim that assigning object properties to arrays is bad practice.

  25. Sebastian: what is the point of having a dynamic, prototype-based OO language then?

    Andrew, great post, but somewhat ill-titled. Using Array instances for key-value pairs is not harmful, extending Array.prototype is not harmful, but combination of both is. You should have stressed the latter in your post more directly.

  26. Pingback May 19th, 2006
    at 9:25 am
  27. Great post! I´ve coding that wrong since I can remember. Thanks for clarifying that…

  28. @Justin - I think everyone should use Base. ;-) But yes, I think it would make Prototype more extendible.

  29. Javascript just sucks. Especially because of for..in and Object().

    The only way to improve the situation is a JavaScript replacement. Proably a mix of python and javascript, but without “prototype” (as of in javascript, not the framework).

  30. @Armin: Get used to prototype-based inheritance, because it’s not going anywhere in JavaScript 2. But JS2 will also have some Pythonic stuff: better iterators, array “comprehensions” (syntactic sugar), and a couple other things.

    @Everyone Else: Some people are saying, “No, that’s not the source of the problem, [this] is,” and most of these comments I agree with. I agree that this post is somewhat of a simplification, but my intent was simply to let people know that they don’t have to choose between Prototype and hashes/dictionaries/whatever.

    I agree that there is no “ideal” hash implementation for JavaScript, at least not a native implementation. I agree that for..in has some big flaws; I agree that prototype inheritance has some big flaws; and I agree that these flaws combine to form a flaw greater than their sum. But I’ll leave these issues for another day and another article.

  31. Does this mean that the whole JSON data exchange format is flawed since it uses ‘associative arrays’?

    http://en.wikipedia.org/wiki/JSON

  32. @Beth: JSON stands for JavaScript Object Notation. It uses Object, not Array, so it’s OK.

  33. I am certainly confused now, and very, very happy I no longer program in Javascript! I must say, however, that there’s nothing quite like describing a problem and not providing a good, clear solution.

  34. Just learn the freaking language. In the end it depends on what you want to do. Arrays are great, Objects are great, Prototype is great, it just all makes sense if you sit down and read a little before writing lines and lines of code with a language you don’t understand.

  35. @Glardo: “Use Object for hashes, not Array.” What’s unclear about that?

    @Cyril: Exactly. JavaScript, unfortunately, is a lot like a natural language like English: people didn’t learn it from a book, they learned it by example. But without formal discussion of English, you wouldn’t know your nominative from your subjunctive, and without formal discussion of JavaScript most people don’t know Array from Object. We all need to go to JavaScript School.

  36. Javascript is to English as “Array() as Hash” style coding is to Ebonics.

    It’s hip to talk wrong! ;)

  37. Why not use Dojo?

  38. @He-Man: Because I like Prototype better. Dojo and Prototype work different ways; one way seems to make sense to some people, while the other way makes sense to the rest, and that’s fine by me. It means that everyone loves their tools instead of being ambivalent toward a solution that tries to be everything to everybody.

  39. From your description I have to conclude JS does not have associative arrays at all.

    It has syntax (similar to many template languages) for accessing an objects members i.e. obj["membername"]

    It’s wrong to call them associative arrays whether based on Object or Array or anything.

  40. Unfortunately “official” JSON extension also uses Object.prototype… :( http://www.json.org/json.js

  41. Hmmm. I wrote about this a bit ago (http://brito.mindsay.com/associative_arrays.mws). It’s not necessarily “wrong” to use associative arrays, it’s just that it’s a misconception. You’re NOT using an associative array, you’re using the Object from which Array inherits from, and by doing so and calling it something else, you’re showing you don’t understand the language well. The reason I find to be most convincing is that by extending Array.prototype AND using an array as a hash you’ll get a mess.

  42. Pingback May 22nd, 2006
    at 2:38 pm
  43. It seems you can use for..in but you have to be careful.

    for (var i in obj) if (obj.propertyIsEnumerable(i)) foo(obj[i]);

    p.s. Nice commenting system. Very slick.

  44. @Shane: that won’t work. If the property is not enumerable, it won’t be iterated over in the for..in loop to begin with, so your version produces the same outcome.

    There are a dozen different ways to make Object.prototype extensions and for..in loops coexist, but they all involve changing the way you loop, and that only helps you as far as your own code is concerned. Any third-party scripts and libraries will expect for..in to work a certain way, and modifying Object.prototype will break that way.

    Thanks for the kind words.

  45. Of course, this comes about way after the fact and you’ve written this almost ten days ago. From what I know about associative arrays used in this matter, they are called the ’subscript notation’ as opposed to the ‘dot notation’. I don’t necessarily think that they’re wrong per sé. For instance obj‘func’ really doesn’t seem so bad (aside from the fact that it just looks funny) but nonetheless harmful as you stated in this post… After all (however), someone had to write it, and it’s good that it was someone who knew what they’re talking about.

    Very rarely do I like reading people’s rants, but this indeed was a good one :) Cheers

  46. Pingback Jun 8th, 2006
    at 4:54 pm
  47. I liked the post and the disussion which followed in comments. I got lots to learn and many links of interest embedded in the comments.

    I also have written a client side validation framework and was about to release first version. This post has made me rethink on the code I have and to restructure the code for checking it for points covered in the post.

    Thanks

    Tushar

  48. Object is the base, Array extends Object … then where is the problem ?

    If you have an array and you use a for in loop maybe your’re doing something wrong (if you use for in for every array) …

    for(var i = 0, j = arr.length; i

  49. Hello. I’m Spanish I speak little English My code based on forEach.

    Array.prototype.for_each= function(f,obj){

    var lg = this.length; var temp = this; if(lg <= 0){

    for(var i in this){
        if(this[i] == Array.prototype[i])continue;
        else if(this[i] == Object.prototype[i])continue;
        else if(this[i] == Function.prototype[i])continue;
        else if(this[i] == Boolean.prototype[i])continue;
        else if(this[i] == RegExp.prototype[i])continue;
        else if(this[i] == String.prototype[i])continue;
        else if(this[i] == Date.prototype[i])continue;
        else if(this[i] == Number.prototype[i])continue;
        else if(!isNaN(i)) continue;
        temp[lg] = {key:i,"value":this[i]};
        lg++;
    }
    
     for (var i = 0; i &lt; lg; i++) {
      f.call(obj,temp[i].value, temp[i].key, this);
    }
    this.length = 0;
    }
    else{
        for (var i = 0; i &lt; lg; i++) {
         f.call(obj,this[i], i, this);
        }
    }
    //
    

    }

    function write_contacts( element, index, array){ document.write(”[" + index + "] = ” + “{name:” + element.name + “, email:”+ element.email+ “}<br>”); if(element.email == “bkn3@columbia.edu”) array[index].email = “brad@columbia.edu” ;

    }

    var contacts = new Array(); contacts["Jim Jones"] = {name: “Jim Jones”, email: “jim@gmail.com”}; contacts["Brad Neuberg"] = {name: “Brad Neuberg”, email: “bkn3@columbia.edu”};

    contacts.foreach(writecontacts); contacts.foreach(writecontacts);

    function write_number(element,index, array){ document.write(”[" + index + "] = ” + element + “<br>”); }

    var number = ["zero","one","two"]; number.foreach(writenumber);

    Good bye.

Painfully Obvious was built with WordPress, Prototype, Slicehost, and other accoutrements. Colophon →