3

Can someone break down what's going on here clearly?

function timerCheck() {
    for(var i=0; i<5; i++) {
        setTimeout(function() {
            console.log("Hello" + i);
        }, 3000);
    }
}

So as some of you may know, calling this function will not work as expected. What will end up happening is that this function will get called 5 times all at once with i set to 5 each time. This will be the output after 3 seconds:

Hello5
Hello5
Hello5
Hello5
Hello5

I also understand that using the setInterval method is the right way to approach this kind of problem, but I am curious what's going on under the hood here. I really want to understand how Javascript works. Please note that I do not have a computer science background, just a self-taught coder.

Shaan
  • 863
  • 2
  • 12
  • 25
  • possible duplicate of [Javascript closure inside loops - simple practical example](http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Felix Kling Nov 19 '11 at 21:00
  • The previous question asks for a solution. I am looking for a thorough explanation of what's actually going on. So this is not a duplicate of that. – Shaan Nov 19 '11 at 21:07

3 Answers3

2

This might help you understand what's going on better:

function timerCheck() {
    for(var i=0; i<5; i++) {
        console.log("Hi" + i);
        setTimeout(function() {
            console.log("Hello" + i);
        }, 3000);
        console.log("Bye" + i);
    }
}

You'll see

Hi0
Bye0
Hi1
Bye1
Hi2
Bye2
Hi3
Bye3
Hi4
Bye4

Immediately printed to the console, because all five iterations of the loop finish very quickly, and then after five seconds you'll see:

Hello5
Hello5
Hello5
Hello5
Hello5

because the timeouts (which were all set at approximately the same time) all occur at once, and since the loop already finished: i == 5.

This is caused by the scope of i. The variable i has a scope of everywhere after it is declared in timerCheck(); There is no local i inside your anonymous function in setTimeout set there is no var i, and i isn't given as an argument to the function.

You can fix this easily with a closure, which will return a function that has a local copy of i:

function timerCheck() {
    for(var i=0; i<5; i++) {
        setTimeout((function(loc_i) {
            return function() {
                console.log("Hello" + loc_i);
            };
        })(i), 3000);
    }
}

Which will output:

Hello0
Hello1
Hello2
Hello3
Hello4

To understand this:

(function(loc_i) {
    return function() {
        console.log("Hello" + loc_i);
    };
})(i)

You have to know that a function can be executed immediately in Javascript. IE. (function(x){ console.log(x); })('Hi'); prints Hi to the console. So the outer function above just takes in an argument (the current value of i) and stores it into a local variable to that function called loc_i. That function immediately returns a new function that prints "Hello" + loc_i to the console. That is the function that is passed into the timeout.

I hope that all made sense, let me know if you're still unclear on something.

Paul
  • 139,544
  • 27
  • 275
  • 264
  • Ok so let me see if I got this. The loop runs really fast and calls the setTimeout function 5 times around pretty much the same time (because the loop ran so fast). Therefore, while you wait for the setTimeout function to execute, the i already reaches 5. However, you can store the i locally each time by setting up a closure each time, but this will all still run at once right? – Shaan Nov 19 '11 at 21:33
  • @Shaan Yeah, I think you got it. Try changing the `console.log` to: `console.log("Local: " + loc_i + ", i: " + i);`. In the second version and you'll see that both `loc_i` and `i` are defined in the inner function. – Paul Nov 19 '11 at 21:43
  • Yes I see it now. The loc_i points to the closure environment, and the i points to the i set in the function timer scope. Thanks! – Shaan Nov 19 '11 at 22:01
1

Variable scope in JavaScript is limited to functions.

In your example, the variable i is declared inside of timerCheck. This means that at the end of the loop, i will be equal to 5.

Now, adding in the call to setTimeout doesn't change the fact that i is scoped to timerCheck and that i has been modified to equal 5 by the time the code inside each setTimeout call has run.

You can create a function that "captures" the value of i so that when you call it from inside your loop you get new variable scope for the setTimeout call:

function createTimer(j) {
    setTimeout(function() {
        console.log("Hello" + j);
    }, 3000);
}

function timerCheck() {
    for(var i=0; i<5; i++) {
        createTimer(i);
    }
}

Since createTimer takes a parameter j, when you pass i from inside the for loop in timerCheck to createTimer, j is now scoped to createTimer so that each setTimeout call has its own j.

Andrew Whitaker
  • 124,656
  • 32
  • 289
  • 307
0

This is actually an add-on to Andrews answer

If you try to set a variable that sets the output it explains the scope also.

function test()
{
    for(var i=0; i<5; i++) {
    t = "Hello " + i + "<br/>";
    document.write(t);
        setTimeout(function() {
            document.write(t);
        }, 3000);
    }
}

as you can see the writes will be as expected but at the time the setTimeout fires the t variable will be the last set. Which will be Hello 4.

so the output will be

Hello 0
Hello 1
Hello 2
Hello 3
Hello 4

from the loop and

Hello 4
Hello 4
Hello 4
Hello 4
Hello 4

from the setTimeout

Burrhus
  • 156
  • 6