Thursday, January 24, 2013

Closures in loops - Javascript Gotchas

When you do enough Javascript programming, it’s difficult to avoid learning about this sooner or later, but I still feel like it’s not well known enough. If you learn about it from a blog post like this, you’re one of the lucky ones. Once upon a time I spent hours going crazy thinking the world didn’t make sense anymore - that or every Javascript implementation had a serious bug. I was trying to figure out why it was that a closure within a for loop seemed to have the same value for every iteration of the loop - it just wasn’t changing even though the loop was iterating properly. I was baffled. Isn’t a closure supposed to capture the current value of variables within it’s accessible scope?

Here’s a simple piece of code you can run to see what I’m talking about:

for(var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 0);
}

If you run that in a Javascript console, it’ll output something like this:

3
3
3

Wait! What? Ok, so maybe I gave it away by wrapping it in a call to setTimeout. What’s happening here? It turns out JavaScript is single threaded so it keeps an event queue where it queues up things to do. The closure created in each loop iteration is queued to run as soon as the rest of the current execution context finishes and CPU time is returned to the event loop. setTimeout here serves to simply defer the execution of each closure until after the loop finishes running. By that point the final value of i is ‘3’. Keep in mind that i was declared before the loop started. It’s scope is external to the loop.

Ok, that makes sense, but what if I do something like this:

for(var i = 0; i < 3; i++) {
    var j = i;
    setTimeout(function() {
        console.log(j);
    });
}

Now we’re declaring a variable j inside the loop that is a copy of i. Well, it turns out that the scope of j here is also external to the loop. It will continue to exist outside the loop and thus only ever has one value which changes as the loop iterates. This in turn means that when a deferred closure runs, the latest value will be the one that all the closures from this loop end up with. In Javascript it is very common practice to pass callbacks around that are deferred until something completes or some event occurs. So like I said, sooner or later you’re likely to run into this problem. It’s gotten me more than once, even though I knew about it.

So what can I do then to make my closures work right in a loop?

I’m glad you asked! There are a few different ways you can handle it, but the basic concept is that you have to capture the value of i in each iteration of the loop. By far the cleanest solution is to just use an iterator function like _.each or $.each from the Underscore or jQuery library:

_.each(_.range(3), function(i) {
    setTimeout(function() {
        console.log(i);
    });
});

If you already have a loop that you don’t want to convert to use an iterator function, all you have to do is wrap your closure in a closure in which you define new variables which capture the current value of the variables that change on each iteration. Got that? The trick to capturing the variables is making sure your outer closure executes immediately during the current iteration of the loop. You can use one of these two similar approaches:

Method 1:

for(var i = 0; i < 3; i++) {
    (function() {
        var num = i;
        setTimeout(function() {
            console.log(num);
        });
    })();
}

Method 2:

for(var i = 0; i < 3; i++) {
    (function(i) {
        setTimeout(function() {
            console.log(i);
        });
    })(i);
}

And that’s it! The world makes sense again! Happy coding :)


UPDATE - 10/7/2013

There is now another simpler solution to this problem since the let keyword is now supported in both Firefox and Chrome. The let keyword is used in place of var to scope variables to the block it’s used in. Similar to one of the above examples that does not work, you can swap var for let and it works magically, like so:

for(var i = 0; i < 3; i++) {
    let j = i;
    setTimeout(function() {
        console.log(j);
    });
}

This works because we declare a new variable, j, which we set to the value of i which is captured by the closure within the loop, but it doesn’t continue to exist beyond the end of one iteration of the loop since it’s scoped locally.

No comments:

Post a Comment