Friday, May 19th, 2006
Javascript Associative Arrays considered harmful
In Javascript, you can treat any object as an associative array, similiar to a Map or Hash structure in other languages. So its just a set of key/value pairs - in JS you can add any arbitrary property on the fly. The below example is using an Object, but you could use an Array or RegExp or FooType.
-
-
var hardcoreBands = new Object();
-
hardcoreBands["mathyGoodnes"] = "Dillinger Escape Plan";
-
hardcoreBands["legendary"] = "Converge";
-
hardcoreBands["fashionistas"] = "Every Time I Die";
-
-
for(description in hardcoreBands) { // print out the bands with descriptions
-
alert(hardcoreBands[description] + " == " + description);
-
}
-
Andrew Dupont has an insightful post warning against the use of the javascript Array type for associated arrays, when a simple Object would be better. He says:
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.
He argues that since Arrays (the big "A" type) in JavaScript are meant to represent numerically indexed things, using them as an associative array violates the principle of least surprise. Array.length doesn't count properties added in the above manner. so the above example would return 0. The Array constructors also don't allow for string keys, further demonstrating that they are really meant for numeric indexes.
The other problem with using Arrays as associative arrays means you can no longer extend Array.prototype, which is what Prototype 1.5 does to great effect. Prototype did break Object associative arrays in 1.4 with additions to Object.prototype, something that is fixed in 1.5 after much wailing and gnashing of teeth. Some might argue extending any of the built-in objects' prototypes is bad form, but those people are wrong.
So use Object for associative arrays, and use Array for numeric arrays. Of course, there will always be exceptions, "no best practices", yadda yadda - but just be aware of the danger and possible confusion if you do violate this.












Is “type” a constant or some sort of magical method? Must be in the new release of prototype. :)
You can’t really consider JavaScript objects as analogous to HashTables etc. because the keys may only be strings. If you try and use Objects as keys then all Objects will map to the same value, since when you assign a non-string key JavaScript just calls toString on it, meaning all your keys will simply be stored as “[object Object]“.
:)
Good observation Tim. What do you recommend? Writing a hashtable class of our own?
Steve: heh, thanks - fixed.
Tim: true, its like a limited version of a HashTable I suppose.
[...] In talking about Javascript Associative Arrays considered harmful, Ajaxian mentions that the Prototype library no longer extends Object.prototype as of 1.5. Welcome to the world of compatible JavaScript! (They do extend Array.prototype, but that cause far fewer headaches, if any.) [...]
No one can be wrond when it comes to opinions, especially concerning Javascript. Let’s keep the unprovable blanket statements to a minimum. Everyone is entitled to their opinion, and the fact that its so easy for people who don’t know what they’re doing (and even people that do) to break functions that other people rely on is a legitimate concern.
Just because you decide to override the Object class to output your Mom’s name when toString() is called, doen’t mean everyone likes your Mom.
Stupid fingers … I meant wrong, no one can be wrong
Also, I don’t think arrays should ever store the number 2.7. I demand that everyone modify their code so that the value 2.7 is never stored in an array! Obey! Obey! Obey! Obey!
if you add a decent toString() to your objects you could use an object as a hash key I guess.
why make such a big issue out of this… arrays are objects as well… they deserve to be abused just like regular objects… whether or not it’s smart to do so… no, but I guess people are doing far more stupid things than this.
Hmm, interesting standpoint but as far as I know, these are all internally equivalent:
var a = {'a':0, 'b':1, 'c':2};
var b = new Array();
b['a'] = 0;
b['b'] = 1;
b['c'] = 2;
var c = new Object();
c.a = 0;
c.b = 1;
c.c = 2;
So how can one be better than the other?
@Menno: Actually, in your example, a.length is undefined and b.length=0 so they aren’t internally equivalent.
My blanket statement was meant to be in jest, I realize there are contexts where changing the global objects is dangerous. In general, on small, experienced teams, extending builtins can be a very good thing - if you are on a corporate team who is just learning javascript, then I think it might not be such a good idea.
I agree that there are certain cases where it is time-saving to modify built-in type behaviours, and there are even cases when it makes sense. These are generally in tight-knit, closed environments when you have people that know what is happening. these are also cases where you meddling does not affect any other projects. I believe that all the frameworks out there that should not be screwing with objects as much as they do. I really don’t think my Array object needs 30 new methods so that I can fade a DIV.
Modifying the native objects is fine if you control all of the JavaScript code that runs on your web page. But if your code may be combined in the same page with code from other sources, you have a problem:
Prototype vs. Web 2.0
sounds more like a problem with array.length rather than a bad practice. associative arrays are common for most any scripting language, since when did it become a bad practice?
I think using an object is a worse approach because it consumes more memory on the client machine.
A JavaScript
Arrayis anObject. You wouldn’t save any memory by using anArrayinstead of anObject.See my article linked above for more details.
[...] Ajaxian » Javascript Associative Arrays considered harmful (tags: Javascript programming) [...]
Oh no, here we go again. Are we talking here about a general language issue or implementations in some arbitrary case. The more you restrain possible options in a programming language, the less usable it becomes in different situations. When you are leading a programmers team in a software project, then you should say what is allowed and what is not (ie. “bad practice”). When you are designing a general use programming language then you should allow as wide range of functionality to a programmer as possible (without compromising simplicity ofcourse).
There are a number of significant disadvantages of using Arrays as hashtables instead of Objects. The biggest one is that any extended prototype functionality added to the Array object will break any for…in loop unless you test for enumerability. The length() problem can be gotten around in the same manner by adding a size() method to the Array object that does the same test:
Array.prototype.size = function() {
var i = 0;
for (var j in this) {
if (this.propertyIsEnumerable(j)) {
i++;
}
}
return i;
}
That’s fine for getting the number of elements, but you’re still buggered on the loops. Each and every for…in loop has to have the same this.propertyIsEnumerable(var) test unless you want breakage. Being the lazy person that I am, I’d rather save myself the trouble of writing all those extra, needless lines of code. But that’s just me.
So how do you iterate over the elements of this superior hash map called an Object?
Nearly a year later, the answer is: ‘for/in’. (Also, this answer was right above the question for weeks.) The question you really want to ask is how to get the count of enumerable properties in an object without looping over all of them, which I can only assume is hideously slow.
Here it is (for what is worth after such a time.length …) a little script to create a working associative array:
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ”); }
String.prototype.startsWith = function(s) { return this.substring(0,s.length - 1) == s; }
String.prototype.endsWith = function(s) { return this.length >= s.length && this.substring(this.length - s.length) == s; }
function AssocArray(){
}
AssocArray.prototype.size = function() {
var result = 0;
for(i in this){
if(this[i].startsWith)
result++;
}
return result;
}
AssocArray.prototype.keys = function(){
var result = new Array();
for(i in this){
if(this[i].startsWith)
result[result.length] = i;
}
return result;
}
AssocArray.prototype.values = function(){
var result = new Array();
for(i in this){
if(this[i].startsWith)
result[result.length] = this[i];
}
return result;
}
And some test to the above:
var taa = new AssocArray();
taa.testa = ‘value1a’; // or taa['testa'] = ‘value1a’;
taa.testb = ‘value2a’; // …
alert(’taa.size = ‘ + taa.size());
var s = ”;
var k = taa.keys();
for(i in k){
s += ‘;’ + k[i];
}
alert(’Keys are ‘ + s);
s = ”;
var v = taa.values();
for(i in taa.values()){
s += ‘;’ + v[i];
}
alert(’Values are ‘ + s);
for(i in taa){
if(taa[i].startsWith)
alert(i + ‘=’ + taa[i]);
}
Note the test for startsWith function in the for..in loops (otherwise the functions themselves are also counted, so I test to the current value to be a String, so it has my added startsWith function)
The trim() and endsWith() functions are not used here, but might also be useful.
Hope this can be useful :)