I saw this tweet yesterday and was reminded , once again, of how misunderstood the return value of a DOM method like document.getElementsByTagName
is. Normally, it doesn’t matter that folks don’t really understand the return value fully, because most of the time the code just works. Where it breaks is interesting, so it’s worth taking a look at it in a little bit of depth.
Typically people do things like this:
var anchors = document.getElementsByTagName('a');
for (var i = 0; i < anchors.length; i++) {
anchors[i].addEventListener("click", beAwesome, false)
}
Common pattern. Get a list of the anchors on the page, loop through and add an event to each.
If asked, I’d be willing to bet most people would assume that the list they’re interacting with is an Array
. It’s got a length
and you can index it- that’s got to be an Array
, right?
Surprise! š
It’s a NodeList
.
Try running some Array
specific methods on that collection. It will crash and burn. Try these:
<script type="text/javascript">
window.onload = function() {
var anchors = document.getElementsByTagName('a');
var woot = anchors.pop();
}
</script>
<script type="text/javascript">
window.onload = function() {
var anchors = document.getElementsByTagName('a');
var newA = document.createElement("a");
anchors.push(newA);
}
</script>
Both will error. Something along the lines of
anchors.pop is not a function
Why is this important? I mean, if it walks like a duck (it’s got a length
), talks like a duck (it’s got an index
) it’s basically a duck right?
Well, while it’s probably okay to think that way most of the time (as in things won’t break), you pay a performance penalty for misunderstanding the above code. Beyond that, there are circumstances in which it can be catastrophic to treat a NodeList
like an Array
.
Let’s look at why.
The NodeList interface provides the abstraction of an ordered collection of nodes, without defining or constraining how this collection is implemented. NodeList objects in the DOM are live.
The last sentence is the important bit. It’s live. Live like a grenade? Sometimes.
I wrote about the performance issue previously. The basic gist is that with a DOM method that returns a NodeList
the lookup is performed every time it’s referenced. So even if you assigned it to a variable, you’re not actually saving anything- it still has to examine the document every time. That’s expensive.
The second bit, the catastrophic bit- I wrote about that as well. Here’s some example code:
<!DOCTYPE html >
<html >
<head>
<title>Endless Loop</title>
<script type="text/javascript">
window.onload= function(){
var anchors = document.getElementsByTagName("a")
for (i=0; i<anchors.length;i++) {
var newA = document.createElement("a");
newA.setAttribute("href","http://www.example.com");
newA.appendChild(document.createTextNode("new link"));
document.getElementById("anchor-holder").appendChild(newA);
}
}
</script>
</head>
<body>
<p id="anchor-holder"><a href="http://www.drunkenfist.com/">my one link</a></p>
</body>
</html>
The above code grabs all the anchors on the page and loops through the collection and each time through the loop it adds another anchor to the page.
Run that, and your browser will crash.
Why? Remember, it’s live. Like a grenade. Which means every time you add a new a
to the document anchors.length
increases by 1. Which means infinite loop.
FTW.
To do the above you need to copy the initial collection of anchors into a static array.
Which reminds me, I saw mention today of a proposed NodeList.toArray()
function. Browser teams? Please implement that. That would be sweet.
Rewriting the death code with such a method would look like this.
<!DOCTYPE html >
<html >
<head>
<title>Endless Loop</title>
<script type="text/javascript">
window.onload= function(){
var anchors = document.getElementsByTagName("a").toArray()
//anchors is static now. Let's do that loop and not crash
for (i=0; i<anchors.length;i++) {
var newA = document.createElement("a");
newA.setAttribute("href","http://www.example.com");
newA.appendChild(document.createTextNode("new link"));
document.getElementById("anchor-holder").appendChild(newA);
}
//that was awesome!
}
</script>
</head>
<body>
<p id="anchor-holder"><a href="http://www.drunkenfist.com/">my one link</a></p>
</body>
</html>
Like I said. Sweet.
Well brought out differences between NodeList and Array – thanks for clearing the confusion!