A collection is not an array

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. However NodeList only has one property (its length, which is also available for an array), and one method (the item() 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 NodeList is a reference to a collection of objects, and if that collection changes (for example, elements are added or removed) the NodeList will 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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://autisticcuckoo.net/ AutisticCuckoo

    Good summary, James! That last point is quite important if you’re dynamically modifying the collection inside a loop.

  • http://www.mikehealy.com.au cranial-bore

    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) ?

  • Tino Zijdel

    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);

  • kyberfabrikken

    A NodeList is a reference to a collection of objects

    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.

  • http://www.brothercake.com/ brothercake

    @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?

  • Eric Meyer

    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?

  • http://www.brothercake.com/ brothercake

    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..in loop will include that method, because this loop iterates through all properties of an object, where a for loop 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 hasOwnProperty to 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?

  • Juan Mendes

    A better performance version, push is not very efficient
    Note:I don’t care about writing into prototypes except for Object:


    NodeList.prototype.toArray = function() {
    var ary = [];
    for(var i=0, len = this.length; i < len; i++) {
    ary[i] = this[i];
    }
    return ary;
    };

  • http://www.brothercake.com/ brothercake

    Why do you care about prototyping to Object, but not to other objects?