0

I thought I understood closures well with Javascript, but apparently I don't. The following code doesn't work. How do I get it to print out all the numbers from 0 through 9 in the console after a 1 second delay? Currently it just prints "undefined" ten times.

NOTE: I'm not looking for a simpler workout to printing out numbers after a delay. This question is about understanding closures.

<script>
for(var i=0;i<10;i++){
    setTimeout(function(i){console.log(i)}, 1000)
}
</script>
J-bob
  • 8,380
  • 11
  • 52
  • 85

3 Answers3

2

Closures are important in JavaScript, but it is important to understand what exactly you are closing over. To rework your current code (kind of).

function(){
    var i;
    for(i=0;i<10;i++){
        setTimeout(function(){
            console.log(i);
        }, 1000);
    }
}

In this example, your code is basically closing over var i;, which means that when the timer runs, it will read the value of var i; and print that out. In this case, as you have seen, when the timer runs, the loop has finished and the value is 10.

What you want to do is create a new function scope that captures the value of i at a specific time.

function(){
    var i;
    for(i=0;i<10;i++){
        (function(iInner){
            setTimeout(function(){
                console.log(iInner);
            }, 1000);
        })(i);
    }
}

This example will create a new anonymous function, and then calling it immediately in the loop, and pass the current value of i into it, so that when your timer reads iInner, it will read the value that was passed into the function, rather than the value from var i;. You can also just call iInner i if you want, but I've used two different names for clarity.

There are also helpers that you can use, like .bind that will essentially automatically create a new anonymous function for you, and pass in arguments like this.

function(){
    var i;
    for(i=0;i<10;i++){
        setTimeout(function(iInner){
            console.log(iInner);
        }.bind(null, i), 1000);
    }
}

<func>.bind will take the value of i and return a new function that passes those args through to <func> when called, and you avoid having to create another layer of nesting.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
0

You've defined the callback function to take parameter i, which is masking the closurable i declared in the for loop. So change it to:

<script>
for(var i=0;i<10;i++){
    setTimeout(function(){console.log(i)}, 1000)
}
</script>

Edit: Sorry, I wasn't looking at the general intention of your question. In terms of the way closures work, the function object captures a reference to any variable in the enclosing scope that is referred to from inside the function body, and which does not bind to any local variable (parameter or explicit var declaration) inside the body. That's why your original attempt didn't work; the i was binding to the local parameter.

The function object does not capture the value that the closured variable possessed at the time the function was defined; it captures a reference to it. Thus, you can't iterate a single variable (I'm talking about the var i declared in the for loop) with the intention that the value be closured for each subsequent function definition. Values aren't closured; variables are.

But, you can effectively closure a value by closuring on a temporary local that has the value you want to capture at the time you define the function. It requires creating a new function scope and defining the closuring function inside that scope, closuring around a local (function parameter) that has the value you want:

<script>
for (var i_outer = 0; i_outer < 10; ++i_outer)
    setTimeout((function(i_inner) { return function() { console.log(i_inner); }; })(i_outer), 1000 );
</script>
bgoldst
  • 34,190
  • 6
  • 38
  • 64
0

This is how you do it. You create a IIFE(Immediately Invoked Function Expression), that returns a function that prints to console. Remember the value of i, should be swallowed by the inner function, by passing it as an argument.

setTimeout expects a "function" as a first argument and that's the reason we return a function from the IIFE.

for(var i=0;i<10;i++){

    setTimeout((function(i) {
        return function() {
            console.log(i);
        }
    })(i), 1000)
}

Thanks

Kiba
  • 10,155
  • 6
  • 27
  • 31