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.
Now playing:
Sade - Why Can't We Live Together
(from Diamond Life)
1 Response
#1 Dew Drop - March 17, 2009 | Alvin Ashcraft's Morning Dew said...
17-Mar-09 5:47 AMPingback 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.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response