0

Last night I devoured the book. You Don't Know JS: Scope & Closures.

In the fifth chapter, on Scope Closures, it gives an example of how the mechanism of scope in javascript can cause a for loop to act in unpredictable ways.

According to the text the following for loop displays the number 6 five times.

for(var i=1; i<=5; i++) { setTimeout( function timer(){ console.log( i ); }, i*1000 ); }

This problem is solved by creating a variable j inside of the function timer()that references the current value of i at the time of running of that particular instance of the loop.

for (var i=1; i<=5; i++) { (function(){ var j = i; setTimeout( function timer(){ console.log( j ); }, j*1000 ); })(); }

I can accept this as is and remember to always observe this peculiarity, but I would much rather understand why.

Would someone be kind enough to provide a detailed explanation within the context of the compiler, the engine, and scope as to why the first loop doesn't work?

Andrew
  • 737
  • 2
  • 8
  • 24

2 Answers2

3

The thing is that the closure functions you create within the for loop remember a reference to the variable i. The setTimeout makes the call to the function asynchronous (so it runs after the loop, whatever delay value you put there) and i++ changes the value of i on each iteration. When the closures run, the value will be the same for all of them, since they all have a reference to the same variable. In the second example you create and run an anonymous function which creates its own scope, and by doing var j = i; you make a variable j in the anonymous function's scope which contains a copy of the value of i at this particular moment. The function that is running in setTimeout remembers a reference to the variable j from the scope where it was created, thus it has the value you were expecting.

Personally I'd pass i as argument to this anonymous function as I find it cleaner than the example you showed above:

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

Nowadays, you can use let, but only in new browsers and Node for now; for backward compatibility take a look at Babel.

lupatus
  • 4,208
  • 17
  • 19
  • good answer. One thing that still isn't clear: didn't you say that the function runs after the for loop? If the variable references occur within function won't they be set after the for loop as well and therefore have the same value? What's going on with `var=` to make it run concurrently, even though it's inside of a function that supposedly runs after the for loop? Is it a question of engine vs compiler? How so? Am I missing something here? – Andrew Sep 20 '16 at 22:19
  • @lupatus, I've added a bit of additional explanation to your answer and rephrased some things---I hope you don't mind. If you don't like the changes, I'll happily write a separate answer and revert this one; I just felt like you'd said about what I would have been going to say anyway. – andyg0808 Sep 20 '16 at 22:20
  • @AndÚ the function like `(function () { ... }())` runs in-place, I said that if you put function in `setTimeout` it runs after - setTimeout puts function passed to it to the next run loop (or later if there is some delay), so after all synchronous code. – lupatus Sep 20 '16 at 22:34
  • @andyg0808 thanks, I only removed `mostly` from part about setTimeout cause it's not mostly - setTimeout puts things on the end of the run queue so it always runs after synch code (correct me if I'm wrong, maybe it's problem with my English ;) ) – lupatus Sep 20 '16 at 22:40
  • @AndÚ there is no real concurrency in JS (I mean things are never run parallel), everything runs in single thread, but `setTimeout` put function passed to it after the others. See [this](https://developer.mozilla.org/en/docs/Web/JavaScript/EventLoop) article on how it works. – lupatus Sep 20 '16 at 22:56
  • @lupatus---I put `mostly` because I couldn't tell from the original answer if that was always true or not. Thanks for clarifying. – andyg0808 Sep 20 '16 at 22:58
  • @andyg0808 Yeah, in Node there is `process.nextTick()` in browser you can use just `setTimeout(function () { ... })` without any delay or with zero (browsers, at least when I was last checking, are putting anyway at least 10ms there). Thats the reason why this delay is said to be 'at least' what you put there - the function lands on the end of run loop and when others finished and delay is already met then it will run, but if others will take more time than your delay that it can take more than delay you put there – lupatus Sep 20 '16 at 23:02
1

The i in the for loop is created in the global scope i.e. there only exists one single i which gets incremented. So after the timeout, the global i has already incremented 5 times to a value of 6. Whereas, because each j is created separately inside a closure, there exist 5 separate j local variables - each scoped only to the closure's context, each assigned an incrementing value of i. Hope this explains :)

cloudwhale
  • 524
  • 5
  • 4