JavaScript for C# programmers: getting caught out with closures

Another stop on the road to becoming a JavaScript developer when you know C#. Fire up Firebug in Firefox and follow along.

In this episode we look at some problems we might encounter when using closures.

Recall that, just like anonymous methods in C#, a closure is a binding between a function and the 'environment' in which it's declared. I've been using them a lot in this series, but here's a simple 'counter' example:

var makeCounter = function(start) {
    return {
        next: function() { start++; },
        value: function() { return start; }
    };
};

var counter = makeCounter(42);
counter.next();
console.log(counter.value()); // outputs 43

Nothing too difficult, we've seen many examples just like this before. The makeCounter function takes a single parameter, start, and then returns an object with two methods, next and value. next advances the internal value of the counter and value merely returns its current value. The closure happens because the two functions are referencing the start parameter (that is, a local variable) of the outer function, even after the outer function has terminated. They have both captured start: this is the closure.

You can see this working in the test code: we create a new counter with start value 42, increment it, and then display the current value.

Let's change it so that we return an array of counters:

var makeCounters = function(start, count) {
    var counters = [];
    for (var i = 0; i < count; i++) {
        counters[i] = {
            next: function() { start++; },
            value: function() { return start; }
        };
    }
    return counters;
};

var counters = makeCounters(42, 2);
counters[0].next();
console.log(counters[0].value()); // outputs 43

Not too much has changed, apart from rearranging the code to create the array of counters. The counter objects still have the same form as before (the two methods); all we're doing is defining a new array and then creating as many counter objects in that array as were requested.

Underneath that function, you can see from the test code that it works as before.

Or does it? Add the following test code after the code to test counters[1]:

counters[1].next();
console.log(counters[1].value()); // outputs 44 ???

Something is wrong: the two counter objects in the array are supposed to be independent, and yet they don't seem to be. They seem to be sharing the same captured value.

That is exactly the problem: the closures are not capturing the "current" value of start, they are capturing the actual variable. If one of them makes a change to that captured variable, then the other closures will see the changed value. (In fact, if you look at the original makeCounter, you'll see that we're implicitly assuming this is how it works: both next and value are acting on the same captured variable.)

Now, with the start variable, it's pretty obvious. Let's make it slightly harder to spot the problem by adding an id method to the returned counter objects:

var makeCounters = function(start, count) {
    var counters = [];
    for (var i = 0; i < count; i++) {
        counters[i] = {
            id: function() { return i;},
            next: function() { start++; },
            value: function() { return start; }
        };
    }
    return counters;
};

The id of a counter object is just its position in the array. At least that's what we want it to be. Can you determine by inspection what the following lines will produce?

var counters = makeCounters(42, 2);
console.log(counters[0].id()); 
console.log(counters[1].id()); 

From the discussion we've just had, the answer is obviously not 0, 1. You're doing well if you recognize that they'll both output the same value, and very well if you work out that the value is 2. (Hint: the loop stops when i reaches 2.)

So what to do? We have to isolate the two local variables so that we can capture them separately for each counter object we create. The easiest way to do that is to use another anonymous function and pass the two values in as parameters.

var makeCounters = function(start, count) {
    var counters = [];
    for (var i = 0; i < count; i++) {
        counters[i] = function (start, id) {
            return {
                id: function() { return id;},
                next: function() { start++; },
                value: function() { return start; }
            };
        }(start, i);
    }
    return counters;
};

This is starting to get a little complicated, but bear with me. We're setting the element in the array, not to a function, but to the result of a function we're immediately going to execute. Here's the code with the noisy bits taken out, it'll be easier to see:

counters[i] = function (start, id) {
    // some code
}(start, i);

In other words, we have an anonymous function that takes two parameters called start and id. We immediately call it passing the current value of start for the start parameter, and the current value of i for the id parameter.

Inside this anonymous function, we merely return a new object. The id method returns the value of id passed in, and the other two methods do their stuff on the passed in value of start. Notice that the scoping rules for these methods say that they get their values from the immediate outer function, not from the outer outer function. Their closure is over the nested inner function. They don't "need" any local variables from the outer function and so don't form a closure over it.

The lesson to take away form this is that closures are over local variables, not the current value of those variables. Sometimes it's hard to see that in the thicket of braces and function keywords.

Having assimilated all that, you'll be in a great position (even knowing nothing about jQuery) to say well, duh! to this post.

Album cover for Diamond Life Now playing:
Sade - Why Can't We Live Together
(from Diamond Life)


Loading similar posts...   Loading links to posts on similar topics...

1 Response

#1 Dew Drop - March 17, 2009 | Alvin Ashcraft's Morning Dew said...
17-Mar-09 5:47 AM

Pingback from Dew Drop - March 17, 2009 | Alvin Ashcraft's Morning Dew

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response