6

I have a very trivial question. For a simple loop with setTimeout, like this:

for (var count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + count);
    }, 1000 * count);
}

console gives an output like this:

Count = 3
Count = 3
Count = 3

Not sure why the output like this. Anyone could explain, please?

niksvp
  • 5,545
  • 2
  • 24
  • 41
Kanan Farzali
  • 991
  • 13
  • 23

9 Answers9

6

This has to do with how scoping and hoisting is being treated in JavaScript.

What happens in your code is that the JS engine modifies your code to this:

var count;

for (count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + count);
    }, 1000 * count);
}

And when setTimeout() is being run it will first look in it's own scope after count but it won't find it so then it'll start looking in the functions that closes (this is called closures) over the setTimeout function until it finds the var count statement, which will have the value 3 since loop will have finished before the first timeout function has been executed.

More code-ily explained your code actually looks like this:

//first iteration
var count = 0; //this is 1 because of count++ in your for loop.

for (count = 0; count < 3; count++) { 
    setTimeout(function() {
        alert("Count = " + 1);
    }, 1000 * 1);
}
count = count + 1; //count = 1

//second iteration
var count = 1;

for (count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + 2);
    }, 1000 * 2);
}
count = count + 1; //count = 2

//third iteration
var count = 2;

for (count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + 3);
    }, 1000 * 3);
}
count = count + 1; //count = 3

//after 1000 ms
window.setTimeout(alert(count));
//after 2000 ms
window.setTimeout(alert(count));
//after 3000 ms
window.setTimeout(alert(count));
Henrik Andersson
  • 45,354
  • 16
  • 98
  • 92
4

think about it like that:

AFTER the 1000*n miliseconds are over, what will be the value of count?

of course it will be 3, because the foor loop ended way earlier than the timeout of 1000*n ms.

in order to print 1,2,3 you'll need the following:

for (var count = 0; count < 3; count++) {
    do_alert(num);
}

function do_alert(num) {
    setTimeout(function() {
        alert("Count = " + num);
    }, 1000 * num);
}

a different approach is to make it a closure function (explained well in JavaScript closures vs. anonymous functions)

for (var count = 0; count < 3; count++) {
    (function(num){setTimeout(function() {
        alert("Count = " + num);
    }, 1000 * num)})(count);
}

these two code samples will actually work similarly.

the first sample calls a named function (do_alert) each iteration.

the second sample calls a CLOSURE anonymous function (which is just like do_alert) each iteration.

it's all a matter of SCOPE.

hope that helps.

Community
  • 1
  • 1
geevee
  • 5,411
  • 5
  • 30
  • 48
1

Think about it:

  1. The code executes a loop, in that loop it sets some code to run later.
  2. The loop finishes.
  3. The setTimeout code executes. What's the value of count going to be? The loop finished ages ago...
MadSkunk
  • 3,309
  • 2
  • 32
  • 49
1

This is to do with closure scoping. The same variable count is available in the scope for each of the setTimeout callback functions. You are incrementing its value and creating a function, but each instance of the function has the same variable count in its scope, and by the time the callback functions execute it will have the value 3.

You need to create a copy of the variable (e.g. var localCount = count) inside a new scope in the for loop to make this work. As for doesn't create a scope (which is the cause of the whole thing) you need to introduce one with a function scope.

e.g.

for (var i = 0; i < 5; i++) {
  (function() {
    var j = i;
    setTimeout(function() {
      console.log(j)
    },
    j*100);
   })();
 }
Joe
  • 46,419
  • 33
  • 155
  • 245
  • doesn't work: `for (var i = 0; i < 5; i++) { var j = i; setTimeout(function(){ console.log(j) }, j*100); }` – rofrol Nov 09 '17 at 16:06
  • This is because the 'j' variable is hoisted into the parent scope as the for loop doesn't create one. You need to create a new function scope to isolate `j`: `for (var i = 0; i < 5; i++) { (function() { var j = i; setTimeout(function(){ console.log(j) }, j*100); })()}` – Joe Nov 10 '17 at 10:59
  • so why create copy when you use IIFE? you can just pass variable to it `(function(j) {})(i)`? – rofrol Nov 10 '17 at 11:57
  • Yes you can also do that and it's probably simpler. There's different ways of doing things. I was just modifying an old answer to respond to your point. – Joe Nov 10 '17 at 12:18
1

Easy fix here is to utilize es6 let local variable. Your code will look almost the same except it will do what you expect :)

 for (let count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + count);
    }, 1000 * count);

}

Or you could create a recursive function to get that job done, as following:

function timedAlert(n) {
  if (n < 3) {
    setTimeout(function() {
        alert("Count = " + n);
        timedAlert(++n);
    }, 1000);
  }
}

timedAlert(0);
Uma
  • 836
  • 5
  • 7
  • For a little further explanation, this works because if you create a var variable, the loop will use the same variable each iteration, but when using let, it creates a brand new variable each iteration. – Vincent Jul 14 '23 at 14:54
1

First, setTimeout(function, milliseconds) is a function which takes a function to execute after "milliseconds" milliseconds.

Remember, JS treats functions as objects, so the for(...) loop will initially produce something like:

setTimeout( ... ) setTimeout( ... ) setTimeout( ... )

Now the setTimeout() functions will execute one by one.

The setTimeout() function will try to find the count variable in the current scope. Failing that, it will go to the outer scope and will find count, whose value is already incremented to 3 by the for loop.

Now, starting execution....The first alert shows immediately, as the milliseconds is 0, the second alert shows after 1000 ms, and then the third alert shows after 2000 ms. All of them shows Count = 3

IOstream
  • 113
  • 1
  • 6
0

That's because all the timeouts are run when the loop finished.

The timeout functions then take the current value of count.

And thats always 3 because the for loop has finished.

Praind
  • 1,551
  • 1
  • 12
  • 25
0

That is because by the time the for loop completes its execution the count is 3, and then the set timeout is called.

Try this:

var count = 0; 
setTimeout(function() {
       for (count = 0; count < 3; count++) {
           alert("Count = " + count);
        }
}, 1000* count);
Sheetal
  • 1,368
  • 1
  • 9
  • 15
0

Better solution IS "Forget both Loops and Recursion" in this case and use this combination of "setInterval" includes "setTimeOut"s:

    function iAsk(lvl){
        var i=0;
        var intr =setInterval(function(){ // start the loop 
            i++; // increment it
            if(i>lvl){ // check if the end round reached.
                clearInterval(intr);
                return;
            }
            setTimeout(function(){
                $(".imag").prop("src",pPng); // do first bla bla bla after 50 millisecond
            },50);
            setTimeout(function(){
                 // do another bla bla bla after 100 millisecond.
                seq[i-1]=(Math.ceil(Math.random()*4)).toString();
                $("#hh").after('<br>'+i + ' : rand= '+(Math.ceil(Math.random()*4)).toString()+' > '+seq[i-1]);
                $("#d"+seq[i-1]).prop("src",pGif);
                var d =document.getElementById('aud');
                d.play();                   
            },100);
            setTimeout(function(){
                // keep adding bla bla bla till you done :)
                $("#d"+seq[i-1]).prop("src",pPng);
            },900);
        },1000); // loop waiting time must be >= 900 (biggest timeOut for inside actions)
    }

PS: Understand that the real behavior of (setTimeOut): they all will start in same time "the three bla bla bla will start counting down in the same moment" so make a different timeout to arrange the execution.

PS 2: the example for timing loop, but for a reaction loops you can use events, promise async await ..

Mohamed Abulnasr
  • 589
  • 7
  • 18