Callbacks in Loops

Once every so often, you'll need to create a callback function while inside a loop. Let's say you are trying to register a click handler for every link you have in the page.

var links = documnet.getElementsByTagName('a')
for (var i = 0, len = links.length; i < len; i++){
    // Note: `addEventListener` is standard compliant browsers only
    links[i].addEventListener('click', function(e){
        alert('You clicked on link ' + i)
    }, false)
}

Any time a link is clicked, you'll open an alert popup dialog telling them the index of the link that was clicked on. Pretty useful. Except, it doesn't work.

What will actually happen is the alert popup will say "You clicked on link 5" every time - supposing that you have 5 links on your page. The reason this happens is because each callback function you created(one for each loop iteration) refers to the same the variable i. That is, although var i is declared at the beginning of the for-loop, it is not lexically scoped, i.e. a new version of i does not get creating for each iteration of the loop. In fact, i could have been declared outside of the loop and it wouldn't have made a difference

var links = documnet.getElementsByTagName('a')
var i
for (i = 0, len = links.length; i < len; i++){
    /* blah blah */
}

The workaround for this problem is to create an outer function and pass the parameter i to it - immediately executing it.

var links = documnet.getElementsByTagName('a')
for (var i = 0, len = links.length; i < len; i++){
    !function outer(i){
        links[i].addEventListener('click', function inner(e){
            alert('You clicked on link ' + i)
        }, false)
    }(i)
}

Note: We also labeled the callback handler function inner so as to distinguish the two functions.

The outer function is also called an IIFE - it get created and then immediately executed, and then thrown away. (There are other ways to write it - I just prefer the version with the !) The reason we need it is because we need a new variable scope, and in Javascript, a function is the only way to create one.

It is important to note that outside of outer, i still resolves to the same i that was declared at the beginning of the for-loop, but inside the function, i is declared as a function parameter, making it a local variable of the function - which take precedence over the outside i.

!function(i){  // <-- inside `i`
    /* here, `i` refers to inside `i` */
}(i) // <-- still outside `i`

In fact, let's rename the inside i to ii to make it clearer

var links = documnet.getElementsByTagName('a')
for (var i = 0, len = links.length; i < len; i++){
    !function outer(ii){
        links[i].addEventListener('click', function inner(e){
            alert('You clicked on link ' + ii)
        }, false)
    }(i)
}

Now ii is local to outer and i is on the outside. Because of closure, ii will stick around even when the execution of outer has finished - a click event will happen much later. In other words, inner will hold on to an ii that's been orphaned - which no other code can access. But, that is what makes it all work.

Update

Jonprins made the comment that creating functions can be expensive and so the outer function is best taken outside of the loop. I agree. So with that optimization the code becomes

function addClickHandler(link, i){
    link.addEventListener('click', function(e){
        alert('You clicked on link ' + i)
    }, false)
}
var links = documnet.getElementsByTagName('a')
for (var i = 0, len = links.length; i < len; i++)
    addClickHandler(links[i], i)

It's faster and cleaner too!

Conclusion

If you've followed along this far, you should know just a little more about how closures work and to watch out for the common problem described in this article associated with loops and callbacks. If you are confused or still unconvinced, drop me a comment.

Ever feel like you are wasting your life away debugging?
It doesn't have to be this way. Check out my debugging course.
blog comments powered by Disqus