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.