JavaScript Closures and Loops

JavaScript closures can be kind of weird. The way a closure works within a loop is a fairly common "gotcha" because it doesn't work in the way that it intuitively feels like it should.

Take the following block of code:

for (var i = 0; i < 5; i++) {
    var button = document.createElement("button");
    button.innerHTML = "Button #" + (i + 1);
    button.onclick = function() {
        alert("You clicked on " + button.innerHTML + "!");
    };
    document.body.appendChild(button);
    document.body.appendChild(document.createElement('br'));
}

What does that do? Well, let's test it here:

Pretty simple, right? We're creating five buttons, so we create a loop to do it. In each loop, we create a new button and then set the containing text to contain the button number. Then we create an onclick handler to deal with the button's action when the user clicks on it. The handler is a closure, which means that it "closes over" the containing environment, so it inherits the local button variable we declared in the loop. That means we can use that to figure out what button was pressed in the handler.

Now try clicking on the buttons. Uh oh, we're always getting button #5. Why does that happen?

Because the loop block is only declared once and not each time through. That means that the variable we're defining, button, is updated each time through and each time through the closure's environment contains that same environment with the updated button value.

Fixing that is fairly simple: create a new environment each pass through the loop. We can do that by simply calling a function that returns the onclick handler for us.

for (var i = 0; i < 5; i++) {
    var button = document.createElement("button");
    button.innerHTML = "Button #" + (i + 1);
    button.onclick = (function(button) {
        return function() {
            alert("You clicked on " + button.innerHTML + "!");
        };
    })(button);
    document.body.appendChild(button);
    document.body.appendChild(document.createElement('br'));
}

The new (function(button) {})(button); chunk means that a new environment is created each time, and this new environment contains a separate entry named button that contains the button we want each time we bind a click handler. This means that the button handlers now refer to the button we expect them to:

Topics: