8
/*Test scope problem*/
for(var i=1; i<3; i++){
    //declare variables
    var no = i;
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
}

It alerts "setting 1" and "setting 2" as expected, but after the timeout it outputs "test 2" twice - for some reason the variable "no" is not reset after the first loop...

I've found only an "ugly" workaround:

/*Test scope problem*/
var func=function(no){
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
}
for(var i=1; i<3; i++){
    func(i);
}

Any ideas on how to workaround this problem in a more direct way? or is this the only way?

Brock Adams
  • 90,639
  • 22
  • 233
  • 295
Carlos Ouro
  • 565
  • 6
  • 16

4 Answers4

13

JavaScript does not have block scope, and variable declarations are hoisted. These facts together mean that your code is equivalent to:

var no;

/*Test scope problem*/
for(var i=1; i<3; i++){
    //declare variables
    no = i;
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
}

By the time your timeout function executes, the loop is long finished, with no retaining its final value of 2.

A way around this would be to pass the current value of no into a function that creates a fresh callback for each call to setTimeout. Creating a new function each time means each setTimeout callback is bound to a different execution context with its own set of variables.

var no;

/*Test scope problem*/
for(var i=1; i<3; i++){
    //declare variables
    no = i;
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout( (function(num) {
            return function() {
                alert('test '+num);
            };
        })(no), 500);
}
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • Great answer, it think t fully explains it. I originally thought the scope always worked within the {}'s in Javascript thus the question. – Carlos Ouro Aug 08 '10 at 10:30
2

This is essentially the same as your fix, but using a different syntax to achieve the scoping adjustment.

/*Test scope problem*/
for (var i = 1; i < 3; i++) {
  //declare variables 
  var no = i;
  //verify no 
  alert('setting ' + no);

  //timeout to recheck
  (function() {
    var n = no;
    setTimeout(function() { 
      alert('test ' + n);
    }, 500);
  })();
} 
John Fisher
  • 22,355
  • 2
  • 39
  • 64
1

I'm liking that I can get so much mileage out of this answer.

If you need help applying that answer, let me know.

EDIT

Sure. Let's look at your original code.

//timeout to recheck 
setTimeout(function(){
    alert('test '+no);
}, 500);

See that anonymous function? The one you pass into setTimeout()? It isn't called until the timer says so - which at 500ms is well after the loop has exited.

What you're hoping for is for no to be evaluated "in place" but its not - it's evaluated at the time the function is called. By that point, no is 2.

In order to get around this, we need a function that executes during the iteration of the loop, which itself will return a function that setTimeout() can use in the way we expect it to.

setTimeout(function( value )
{
  // 'value' is closed by the function below
  return function()
  {
    alert('test ' + value );
  }
}( no ) // Here's the magic
, 500 );

Since we create anonymous function and immediately call it, a new scope has been created in which we can close off variables. And that scope closes around value, not no. Since value receives a new (ahem) value for each loop, each of these lambdas has it's own value - the one we want it to.

So when setTimeout() fires, it's executing the function returned from our closure function.

I hope that explains it.

Community
  • 1
  • 1
Peter Bailey
  • 105,256
  • 31
  • 182
  • 206
  • 1
    Yeah, not seeing how this relates to this question. Can you explain a closure to a C/Java developer only just learning Javascript? – Daniel Bingham Apr 28 '10 at 16:08
1

Javascript does not have lexical scoping(the for-loop does not create a new scope), and your solution is the standard workaround. Another way to write this might be:

[1, 2].forEach(function(no){
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
})

forEach() was introduce in ECMAScript 5 and is present in modern browsers but not IE. You can use my library to emulate it though.

airportyh
  • 21,948
  • 13
  • 58
  • 72