I’m occassionally irked by the fact that a collection of DOM elements (more formally called a NodeList) can’t be manipulated like an array, because it isn’t one. However it does look like one, and thinking it is one is a mistake made so often by JavaScript novices that for our upcoming JavaScript Reference I felt it necessary to note this point for every single DOM object that is, or returns, a collection.
You can iterate through a collection like an array:
for(var i=0; i<collection.length; i++)
{
//whatever
}
But you can’t use Array methods like push(), splice() or reverse() to manipulate it.
Except that you can, if you take the next step and convert it into an array. This is in fact trivial:
function collectionToArray(collection)
{
var ary = [];
for(var i=0, len = collection.length; i < len; i++)
{
ary.push(collection[i]);
}
return ary;
}
The code above is fully cross-browser, and is called with the original collection as an argument:
var elements = collectionToArray(document.getElementsByTagName('*'));
However if you only need to deal with browsers that support native object prototyping (Opera, Firefox and Safari 3) then you can simply create a toArray() method of NodeList:
NodeList.prototype.toArray = function()
{
var ary = [];
for(var i=0, len = this.length; i < len; i++)
{
ary.push(this[i]);
}
return ary;
};
Which can then be called as a method of the individual collection:
var elements = document.getElementsByTagName('*').toArray();
There is one obvious disadvantage to this conversion (however it’s done), which is that the resulting array will no longer be a NodeList. Obvious, yes, but relevant because it has two implications:
- It will lose the properties and methods it inherited from
NodeList. HoweverNodeListonly has one property (itslength, which is also available for an array), and one method (theitem()method, which is usually redundent anyway, since members can still be referred to with square-bracket notation). So this loss is not at all significant - It will no longer be a live collection. A
NodeListis a reference to a collection of objects, and if that collection changes (for example, elements are added or removed) theNodeListwill automatically update to reflect that change; conversely our array is a static snapshot of the collection at one point in time, and so won’t update in response to changes in the DOM. Depending on your application, that could be significant.





March 19th, 2008 at 5:47 pm
Good summary, James! That last point is quite important if you’re dynamically modifying the collection inside a loop.
March 20th, 2008 at 11:21 pm
Thanks for that. I knew a collection wasn’t an array, but I thought that for all intents and purposes they were the same. Good to know what the practical difference really is.
Does this mean that you couldn’t add an event listener to an array element (as per this collection->array function) ?
March 21st, 2008 at 12:53 am
Here’s a nice way to convert a nodeList to an array (it uses the Array.slice() method on the nodeList itself):
var nodeArray = [].slice.call(nodeList, 0);March 25th, 2008 at 8:38 pm
This is actually a major gotcha — NodeLists are late bound, which can cause quite some confusion, if you iterate over it and manipulate the DOM at the same time. For this reason alone, it’s usually a good idea to convert it into an array.
March 26th, 2008 at 9:09 am
@cranial-bore: you can still bind events to those elements; how you store their references doesn’t affect the elements themselves at all.
@Tino Zijdel: ah yeah, I’d forgotten about that, you can indeed effectively “steal” methods from Array.prototype. Nice :) Dan Webb talks about that in his metaprogramming chapter of Art & Science of JS.
@kyberfabrikken: not sure what you mean there, can you elaborate?
June 1st, 2008 at 12:35 pm
Is this why doing a for/in loop over a collection causes failures that don’t happen with a straight for loop, and if so, can you explain exactly what goes wrong with the former that isn’t triggered by the latter?
June 4th, 2008 at 6:49 pm
What can go wrong with the former is the appearance of external prototypes of built-in objects, such as the stuff that the prototype library does. If you have something like this:
Array.prototype.myCustomFunction = function() { .... }Then a
for..inloop will include that method, because this loop iterates through all properties of an object, where aforloop merely iterates through the numerically-indexed properties (the memebers of an array are merely a subset of its properties, namely, those properties that have a numeric index).The easiest way around that is to use
hasOwnPropertyto discriminate and ignore the properties you don’t want:for(var i in obj) { if(!obj.hasOwnProperty(i)) { continue; } //now we're okay }Is that the issue you were referring to?