NodeLists and Arrays in JavaScript

JavaScript is an admittedly quirky language, and its almost array-like objects is one of the most glaring issues. And of all those array-like objects, NodeLists are one of the most commonly used ones array-like objects that beginners trip on when they use methods like document.getElementsByTagName().

In this post, I’ll try to explain what NodeLists are, how they relate to arrays in JavaScript and how you can work with them in a comfortable way.

So what is exactly an NodeList? The W3C defines the NodeList as follows:

The NodeList interface provides the abstraction of an ordered collection of nodes, without defining or constraining how this collection is implemented.

Essentially, a NodeList is what you get when you call any method such as document.getElemetsByTagName(), document.querySelectorAll() and such.

We should note here that NodeLists aren’t exactly part of the JavaScript but they are instead part of the DOM APIs the browsers provide through JavaScript. We will come back to discussing the relationship between NodeLists and arrays later in the article again.

Careful readers of the W3C definition will note that arrays in most languages, like JavaScript, are almost what this definition implies NodeLists are: “ordered collection of (things)”. In fact, NodeLists do in fact resemble arrays in a lot of ways. Let’s test things out by firing up our JavaScript console on the Digg homepage.

    > var myList = document.querySelectorAll('.story-item');
      undefined
    > myList
      myList
      [
      <div class="story-item">...</div>
      ,
      <div class="story-item">...</div>
      ,
      [...]
      ,
      <div class="story-item">..</div>
      ,
      ]

Definitely looks like an array from here, if nothing else other than fact that the way our console inspects myList starts and ends with a square bracket. Let’s try some basic array actions with it:

    > myList.length;
      17
    > myList[3];
      <div class="story-item" ></div>​

So far, myList has been talking and walking like an array so we can probably assume that it’s an array of some sorts. However, it all goes to hell when you try to call any of the basic array methods. Suppose we want to slice the array from it’s 3rd element.

    > myList.slice(2) // indexed from 0
      TypeError: Result of expression 'myList.slice' [undefined] is not a
      function.

Wait, what happened? Well, this is where the between NodeLists and arrays in JavaScript start to surface. Let’s try to see if NodeList actually an array (which is actually a tricky problem in JavaScript itself).

    > myList.constructor.toString();
      "[object NodeListConstructor]"

Let’s see what an array, that we definitely know to be an array looks like using the same technique (where we look at its’ constructor).

    > var surelyArray = ['foo', 'bar'];
      undefined
    > surelyArray.constructor.toString();
      "function Array() {
          [native code]
      }"

So those two elements, myList and surelyArray are definitely constructed by different constructors so it’s no wonder that they don’t share the same methods.

In fact, it turns out that NodeLists support accessing elements by their index and they do have length property but that’s essentially where the similarities end. If you want to call any of the array methods on a NodeList, you will just get an error.

Let’s think about why this is the case. If you think about what NodeLists are and the re-read the definition, this (kind of) makes sense. While arrays are essentially a collection of elements held in memory and are part of the JavaScript, NodeLists are live references to actual DOM elements, except for the document.querySelectorAll() method, which returns not live but static NodeLists.

Luckily, you can relatively easily convert NodeLists into arrays so that you can easily call all your favorite array methods like push(), slice() on them.

Let’s see a quick way to convert a NodeList into an array:

    > var myArray = Array.prototype.slice.call(myList, 0);
      undefined
    > myArray.constructor.toString();
      "function Array() {
          [native code]
      }"

Definitely looks like an array! Before we look at what happened with that one-liner, let’s really make sure that this myArray is an actual array, not some other crazy creation.

    > myArray.pop();
      <div class="story-item">...</div>

Not bad. We are definitely sure that we are dealing with an array here.

The one-liner we used before is actually pretty simple. What we are essentially doing here is borrowing the slice() method from the Array’s prototype and applying it to on NodeList, slicing it from it’s beginning, so getting the entire list itself. And as slice() returns an actual array, we end with a real array, as opposed to a NodeList. More explanation on call() works can be found on Robert Sosinski’s excellent post on Binding Scope in JavaScript.

There you have it. Except, if you use Internet Explorer. It turns out that Internet Explorer versions before Internet Explorer 9 cannot handle calling slice() on NodeLists –and in general behave badly on host object interactions.

One way to fix that problem is simple create a new array, iterate over your existing NodeList and push things into your new array.

    > var myIEArray = [];
      undefined
    > for (var i = 0; i < myList.length; ++i) { myIEArray.push(myList[i]); }
      17
    >  myIEArray.constructor.toString();
      "function Array() {
          [native code]
      }"

This is a good start but however there’s a slight issue with that code that James-David Dalton pointed out. It’s possible for a NodeList to have an element in it that has the id length. In that case, the expression myList.length would no longer be an integer but the element with the id length itself. Nevertheless, this is easy to fix.

    > var node, i = -1, myIEArray = [];
      undefined
    > var myList = document.querySelectorAll('.story-item');
      undefined
    > while (node = myList[++i]) { myIEArray[i] = node; }
      <div class="story-item">...</div>

This code involves a bit more trickery. In essence, we are again looping over the elements in the myList NodeList but making sure that a given index is not undefined which is false in JavaScript. The reason we initialize our index variable i to -1 is that we increment the index before we actually try to access the ith index. So if we were to set the i to 0 instead of -1, we would both miss the first element in the MyList NodeList and also start setting the elements in the myIEArray from its 1st index, leaving myIEArray[0] undefined.

There you have it, for real this time, a real array from your NodeList.

One thing is worth mentioning though; when you convert your NodeList into an array, you are no longer dealing with a live NodeList (again, document.querySelectorAll() actually returns not live but static NodeLists) but instead an array of DOM nodes.

Converting a NodeList into an array has some interesting consequences. Going back to our example where we ran the myArray.pop(), you’d not the top Digg story disappearing as your array is just a collection of DOM nodes, not representation of your DOM anymore.

However, the DOM objects themselves are live. So if you were to do something like myArray[0].innerHTML('foo bar'), you would instead change the DOM.

NodeLists are a powerful tool and with more and more browsers getting decent DOM APIs, you might find yourself dealing with more NodeLists instead of jQuery objects and such.

In fact, if you look at the source code for Thomas Fuchs’ Zepto.JS micro-library, you’ll find that it implements arguably the most important aspect of jQuery, the CSS DOM selector using the (relatively) new document.querySelectorAll() method and converting the NodeList to an array for easy further manipulation.

I hope this brief introduction to NodeLists (and arrays) has been useful. Please let me know in the comments if you have any questions or corrections.