var allDivs = document.getElementsByTagName('div');
for(var i = 0; i < allDivs.length; i++) {
document.body.appendChild(document.createElement('div'));
}
Do you see anything wrong in this snippet of code? You would expect it to create one additional div for every div element in the DOM, so it should just double the number of div nodes in the document, right? Well, not really! This is actually an infinite loop. Why? Because the loop's exit condition is never met, as allDivs.length
increases by one with every iteration.
As defined in the DOM standard, HTML collections are "assumed to be live", meaning that they are automatically updated when the underlying document is updated.
So, here is the solution:
Store the collection in a local variable, cache the length outside the loop and then use a local variable inside the loop for elements that are accessed more than once. e.g
var coll = document.getElementsByTagName('div');
var len = coll.length;
for (var count = 0; count < len; count++) {
// if needed cache in here the elements, to work with
var el = coll[count];
// do whatever
}
This will do the magic and solve your problem!
Let's take it one step further. What if you want to touch every element you retrieve from this collection in a way that modifies the live collection. A common example is to replace a class from a collection of elements. e.g
var allDivs = document.getElementsByClassName('class1');
for (var i = 0; i < allDivs.length; i++) {
allDivs[i].className = 'class2';
}
This doesn't work, since allDivs changes with every iteration of the loop. In this case the best solution to go with is to actually copy the collection into an array and work with the array instead.
Something like this:
function toArray(collection) {
var result = [];
var len = collection.length;
for (var i = 0; i < len; i++) { result[i] = collection[i]; }
return result;
}
var allDivs = document.getElementsByClassName('class1');
var ar = toArray(coll); //copies a collection into an array
for(var i = 0; i < ar.length; i++) {
ar[i].className = 'class2';
}
Now let's look at another neat solution to this problem (unfortunately not supported by all browsers, i.e Internet Explorer 6 and 7). The document.querySelectorAll() method, is provided as a native DOM method. What's cool with it? You can provide a CSS selector as an argument and it will return you a non-live NodeList, an array-like object containing matching nodes, which will not represent the live structure of the document. Let me note here that in W3C's DOM specifications NodeLists are live, but in the case of querySelectorAll(), its explicitly specified that the returned Nodelist is static (not-live)! So, with this method we don't need to be as verbose as we used to be, e.g instead of calling
var elmts = document.getElementsById('menu').getElementsByTagName('a');
we can do something like:
var elmts = document.querySelectorAll('#menu a');
and of course there's no need to cache/copy variables, in order to access them.
Last but not least...use a library if possible! Avoid dealing with this problem and let the library (e.g YUI, jQuery) do its magic!
References:
High Performance JavaScript, Nicholas C. Zakas (http://oreilly.com/catalog/9780596802806)