Oct 262010
 

Closures are one of the most powerful features of Javascript. They are also one of the easiest features to mess up if you don’t have a proper understanding of them. I took to studying them better after encountering a strange bug in my application. After delving deeper, it finally made sense.

Simply put, a closure is a feature which allows local variables in a function to be alive even after the function which contained them has gone out of scope. Those of us who come from the strict world of Java/.NET might find this truly unnerving – How can a local variable be still alive even after its container function goes off the stack? But closures are possible in the .NET and Java worlds too. Here is a great article on the subject by the guru Jon Skeet himself.

In Javascript the easiest way to create a closure is to define a function inside another function. The inner function still retains access to all the local variables of its parent function. Keep in mind though, that this access isnt by value but rather by reference. This was the exact situation which happened to me. In the sample code below, I iterate through an array of country names to create a dynamic list of divs. Clicking the country name’s div would trigger an alert message with the country’s name. Seems quite simple to do. Here is the code.

var countries = ["India", "USA", "Brazil", "Netherlands", "China"];

var styleObject = {
    'width': '150px',
    'background-color': '#66CCFF',
    'border': 'solid 1px black',
    'padding': '2px',
    'margin': '10px'
};

$(document).ready(function () {
    wrongUse();
});

var wrongUse = function () {

    for (var i = 0; i < countries.length; i++) {
        var countryName = countries[i];
        var countryDiv = $('<div>' + countryName + '</div>').css(styleObject);
        countryDiv.click(function () {
            alert("The Country you clicked on is " + countryName);
        });
        $(countryDiv).appendTo($('#canvas'));
    }

};

Surprisingly however, irrespective of whatever div was clicked – the alert message always showed “China”. This was because the inner click function was accessing the local variable called countryName through reference and not value. Since for every iteration this variable was changed, only the last value which was China, remained. The interesting part is that the click function is defined in the document.ready(), but is invoked much later. Because of the closure, each click event still maintained a link to the outer function’s local variable whose value was now “China” because of subsequent iterations.

To avoid the closure problem, we can use the Javascript Function constructor (Note the capital ‘F’). This constructor is used to create an anonymous function type and return an object of it. The advantage of this is that the Function constructor doesn’t create a closure with its enclosing type.

var countries = ["India", "USA", "Brazil", "Netherlands", "China"];

var styleObject = {
    'width': '150px',
    'background-color': '#66CCFF',
    'border': 'solid 1px black',
    'padding': '2px',
    'margin': '10px'
};

$(document).ready(function () {
    rightUse();
});

var rightUse = function () {
    for (var i = 0; i < countries.length; i++) {
        var countryName = countries[i];
        var countryDiv = $('<div>' + countryName + '</div>').css(styleObject);
        countryDiv.click(new Function("alert('The Country you clicked on is " + countryName + ".')"));

        $(countryDiv).appendTo($('#canvas'));
    }
};

We see that we get the desired result

As usual, JQuery manages to give us an even easier way to achieve this – the each() function. The each function operates on array types and executes a similar function on each element in the collection. The advantage is that there is no sharing of the scope between the various executions which eliminates the risk of shared variables being modified.

var countries = ["India", "USA", "Brazil", "Netherlands", "China"];

var styleObject = {
    'width': '150px',
    'background-color': '#66CCFF',
    'border': 'solid 1px black',
    'padding': '2px',
    'margin': '10px'
};

$(document).ready(function () {
    jQueryWay();
});

var jQueryWay = function () {
    $.each(countries, function (index, value) {
        var countryDiv = $('<div>' + value + '</div>').css(styleObject);
        countryDiv.click(function () {
            alert("The country you clicked on is " + value);
        });

        $(countryDiv).appendTo($('#canvas'));
       
    });
};