views:

1120

answers:

6

Closures are one of those things which has been discussed a lot on SO, but this situation pops up a lot for me and I'm always left scratching my head what to do.

var funcs = {};
for (var i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

It outputs this:

My value: 3
My value: 3
My value: 3

Whereas I'd like it to output:

My value: 0
My value: 1
My value: 2

What's the solution to this basic problem?

+14  A: 

Try:

var funcs = {};

for (var i = 0; i < 3; i++) {          
    funcs[i] = (function(index) {   
        return function() {          
            console.log("My value: " + index);
        } 
    })(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        
}
apphacker
thanks for this, app. This is the solution I went with in my own code, but I'm accepting the harto's since it's a little clearer, plus he could use the rep :p
nickf
Thank you! I always ended up doing something similar to the accepted solution, precisely when I wanted to avoid declaring another top-level function.This is perfect!
Daniel Magliola
+1  A: 

The value i inside the body of the closure is being bound to the same instance for each closure (that is, to the variable i which is the loop control variable for the for loop). This value is changing as you go through the loop. You need to figure out a way to make sure that it is bound to a different variable that is unique for each closure. The method shown by apphacker is one way, although possibly a little arcane.

var funcs = {};
for (var i = 0; i < 3; i++) { 
    var index = i;                     // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + index); // each should log its value.
    };
}

This method doesn't work - the reason being because var in javascript uses function scope as opposed to other algol derivatives which use block scope. You can use let as a slightly less portable alternative, but it looks like your best bet is to use a factory method to create the closures.

1800 INFORMATION
no it doesn't work: they now all share the same scope on index and all output their value as "2".
nickf
that right, that won't work, check my answer on the why.
eglasius
ah yes delightful. I've updated my answer with further details
1800 INFORMATION
+16  A: 

Well, the problem is that the variable i, within each of your anonymous functions, is bound to the same variable outside of the function.

What you want to do is bind the variable within each function to a separate, unchanging value outside of the function:

var funcs = [];

function createfunc(i) {
    return function() { console.log("My value: " + i); };
}

for (var i = 0; i < 3; i++) {
    funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

Since there is no block scope in JavaScript - only function scope - by wrapping the function creation in a new function, you ensure that the value of "i" remains as you intended.

harto
Ah - same as apphacker's solution - but slightly more verbose...
harto
And less confusing
Darren Clark
Very nice explanation. Saved my day!
Raj
A: 

so the reason your original example did not work is that all the closures you created in the loop referenced the same frame. in effect having 3 methods on one object with only a single 'i' variable. they all printed out the same value

jottos
+3  A: 

What you need to understand is the scope of the variables in javascript is based on the function. This is an important difference than say c# where you have block scope, and just copying the variable to one inside the for will work.

Wrapping it in a function that evaluates returning the function like apphacker's answer will do the trick, as the variable now has the function scope.

There is also a let keyword instead of var, that would allow using the block scope rule. In that case defining a variable inside the for would do the trick. That said, the let keyword isn't a practical solution because of compatibility.

var funcs = {};
for (var i = 0; i < 3; i++) {
    let index = i;          //add this
    funcs[i] = function() {            
        console.log("My value: " + index); //change to the copy
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        
}
eglasius
this doesn't work. "missing ; before statement -- let index = i;"
nickf
@nickf which browser? as I said, it has compatibility issues, with that I mean serious compatibility issues, like I don't think let is supported in IE.
eglasius
...nor firefox. We're talking about javascript right?
nickf
@nickf yes, check this reference: https://developer.mozilla.org/En/New_in_JavaScript_1.7 ... check the let definitions section, there is an onclick example inside a loop
eglasius
@nickf hmm, actually you have to explicitly specify the version: <script type="application/javascript;version=1.7"/> ... I haven't actually used it anywhere because of the IE restriction, it just isn't practical :(
eglasius
you can see browser support for the different versions here http://es.wikipedia.org/wiki/Javascript
eglasius
+2  A: 

Another way of saying it is that the i in your function is bound at the time of executing the function, not the time of creating the function.

When you create the closure, i is a reference to the variable defined in the outside scope, not a copy of it as it was when you created the closure. It will be evaluated at the time of execution.

Most of the other answers provide ways to work around by creating another variable that won't change value on you.

Just thought I'd add an explanation for clarity. For a solution, personally I'd go with Harto's since it is the most self explanatory way of doing it from the answers here. Any of the code posted will work, but I'd opt for a closure factory over having to write a pile of comments to explain why I'm declaring a new variable(Freddy and 1800's) or have weird embedded closure syntax(apphacker).

Darren Clark