views:

194

answers:

5

I'm having a problem with some JavaScript code.

Script

setTimeout(function() {
    for (var i = 0; i < 5; i++) {
        setTimeout(function() {
            console.log(i);
        }, i * 200);
    }
}, 200);

Outputs

5, 5, 5, 5, 5 instead of 1, 2, 3, 4, 5

I can kind of understand why this doesn't work, but I was wondering if someone could explain to me what's happening and why it's not working!

Also, how can this scope problem be overcome?

A: 

Because you are accessing the same variable i in all the functions used in set timeout. The setTimeout function sets the function to fire the number of milliseconds in the future on the same thread as the i variable. The i value isn't copied in the function, the function is referencing the actual variable i when it is fired. Because you have looped through parent function until the i = 5 and this is done before anything else has a chance to fire, they all show up as 5.

Kevin
+2  A: 

Take a look at this question. It might help you understand the scope and closures better, very similar to your question.

Quintin Robinson
Thanks for the great link! It explains it perfectly!
Michael Waterfall
+1  A: 

The variable i exists in the scope of the outer function.

It changes as the loop runs.

The inner function references it.

Try something like this:

var i_print_factory = function (value) {
  return function () {
    console.log(value);
  };
};

var init_timers = function () {
  for (var i = 0; i < 5; i++) {
    setTimeout(i_print_factory(i), i * 200);
  }
};

setTimeout(init_timers, 200);
David Dorward
+2  A: 

You're trying to create a closure containing the variable "i". But closures are only created at the end of a function. So if your functions are created in a for loop, they will all have the values from the last iteration.

You can fix it with something like this:

var createFunction = function(index) {
  return function() {
    console.log(index);
  }
};

for (var i = 0; i < 5; i++) {
  setTimeout(createFunction(i), i * 200);
}

where you return the function from another function.

JW
+3  A: 

The setTimeout callback functions are executed asynchronously, all the console.log calls you make refer to the same i variable, and at the time they are executed, the for loop has ended and i contains 4.

You could wrap your inner setTimeout call inside a function accepting a parameter in order to store a reference to all the i values that are being iterated, something like this:

setTimeout(function() {
    for (var i = 0; i < 5; i++) {
      (function (j) { // added a closure to store a reference to 'i' values
        setTimeout(function() {
            console.log(j);
        }, j * 200);
      })(i); // automatically call the function and pass the value
    }
}, 200);

Check my answer to the following question for more details:

CMS
Thanks for your response and your answer on that other question! It explained it all perfectly!
Michael Waterfall
@Bisbo: You're welcome, glad to help!
CMS
@bdukes: rolled back edit, the last value of `i` is `4`, notice the `i < 5` condition on the for loop.
CMS
i will be 5 - that's the value that terminates the loop (and is also the value reported by the OP)
Andrew Duffy