8

Let me clarify my question. I'm not asking how to make the following code work. I am aware that you can use the let keyword or an iffe that captures its own value of i. I just need clarification on how the value i is accessed in the following code. I read the following blog post about how it is that the following code does not work. Blog post

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

The writer claims that the code will not work, because we are passing the variable i as a reference instead of a value. That is, instead of providing the value of i per iteration we provide the variable to the callback in setTimeout as a reference which. In effect, when the loop terminates and callbacks fire, we will have reference to the variable i which will be 6. Is this how it works?

Here is my understanding. My understanding is that we are not "passing" anything to the callbacks of the setTimeout function, when the loop is executed. We are merely setting up the asynchronous calls. When the closure callback functions do execute, they then look for the variable i based on lexical scoping rules. That is, the closures look in the scope were the callbacks have closure over, which again, in this case would be 6 since it is done after the for loop completes.

Which one is it, does the function resolve the value of i to 6 based on the variable being passed as a reference on each iteration or because of lexical scoping?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Hugo Perea
  • 465
  • 4
  • 15
  • interestingly enough, you still get 66666 if you setTimeout with 0ms pause! – JMP Dec 31 '17 at 07:21
  • @JonMarkPerry That's because JS is single threaded. Timeouts are added to the message queue when the timer fires (i.e. the pause is only a minimum delay). MDN has a great article on [the Event Loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#Zero_delays) where you can read about it – Kiren Dec 31 '17 at 08:04
  • @Kiren; vg, so we wait for the for...loop queue to clear before the sT's are executed, by which time `i` is 6, unless we use closure. – JMP Dec 31 '17 at 08:17
  • you can use `setTimeout(console.log(eval(i)));` – JMP Dec 31 '17 at 08:25
  • "*they then look for the variable i based on lexical scoping rules.*" - how would that be possible without a **reference** to said lexical scope? (Of course, whether the reference is to a whole scope or an individual variable, and when the "lookup according to rules" is done, are implementation details and subject to optimisation). – Bergi Dec 31 '17 at 10:03
  • @JonMarkPerry Yes, but `eval()` should be avoided in 99.99999% of all use cases - certainly this one. – Scott Marcus Dec 31 '17 at 18:50
  • There is a reference to the scope.. I'm asking about a direct reference to the variable i... – Hugo Perea Jan 01 '18 at 01:43

2 Answers2

20

You are correct that lexical scoping is the cause of this behavior. When the timer functions run (which will be after the currently running code completes), they attempt to resolve i and they must look up the scope chain to find it. Because of lexical scoping, i exists only once in the scope chain (one scope higher than the timer functions) and, at that point, i is 6 because, at that point, the loop has terminated.

The var keyword causes variables in JavaScript to have either function or Global scope (based on where that declaration is). In your code, var i causes the i variable to exist Globally (because your code is not inside of a function) and each timer function must resolve that same, single i when they eventually run. Since the timer functions won't run until the loop is done, i is at its last value (6) that the loop caused it to be.

Change var i to let i to create block scope for i to solve the problem.

let creates block scope for the variable. Upon each iteration of the loop, you enter the loop block again and a separate scope is created for i that each timer function gets to itself.

for (let i = 1; i <= 5; i++) {
  setTimeout(function() { console.log(i); }, 1000*i);
}
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
  • for this very reason, i have stopped using var completely. function scoped variables are not the norm when it comes to most c syntax style languages. as the answer says, let enforces block scope. – simon Dec 31 '17 at 06:53
  • 1
    let is also not hoisted either for reference to the OP. there is a lot of internal baggage associated with var. be careful when using it. – simon Dec 31 '17 at 06:56
  • Thanks. I am aware of the solutions. I just wanted clarification on how the value of `i` is resolved. Is it resolved throw lexical scope? When the callback functions are fired do they look for the value of `i` through lexical scope. Since the callbacks have closure over the variable `i` in the outside scope, does the value of `i` get resolved lexically. Meaning that the callback function will first look for the value within it's scope and then within the outside scope? Thanks – Hugo Perea Dec 31 '17 at 07:00
  • @HugoPerea Yes, JavaScript uses lexical scoping. Closures are a byproduct of this. – Scott Marcus Dec 31 '17 at 07:01
  • What do you mean by "references". That's my question. When you say that each call back references the same variable `i` does that mean that when the function executes it does not need to look up the variable throw lexical scope because it already has a reference to it. – Hugo Perea Dec 31 '17 at 07:14
  • @HugoPerea No. It must look up the scope chain to resolve `i`. This is what lexical scoping means. Let me change the word "reference" to "resolve". – Scott Marcus Dec 31 '17 at 07:15
  • Ahh that's all I wanted to know. Thanks – Hugo Perea Dec 31 '17 at 07:16
  • @simon [`let` and `const` are also hoisted](https://stackoverflow.com/q/31219420/1048572) – Bergi Dec 31 '17 at 10:07
  • No, they are not. – simon Dec 31 '17 at 10:23
  • Regardless of what the others in that article say, hoisting requires both declaration /and/ initialization. vars and lets are both initialized as undefined if given no value, however only var is able to be accessed prior to its declaration. Here is a reference: https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/ – simon Dec 31 '17 at 10:36
  • Example to parse in your browser: z = 5; console.log(z); let z; If let was hoisted, the console.log() would output it. – simon Dec 31 '17 at 10:47
  • @PeterMortensen Please limit edits to corrections. The use of a capital "G" when referring to Global scope is quite common and was my intent. – Scott Marcus Dec 31 '17 at 18:35
  • @Bergi It states in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) for `let`: *"In ECMAScript 2015, let bindings are not subject to Variable Hoisting, which means that let declarations do not move to the top of the current execution context."* – Scott Marcus Dec 31 '17 at 18:52
  • @ScottMarcus Apparently [MDN is inconsistent in its usage of the term "hoisting"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let$compare?to=1259455&from=1250513). – Bergi Dec 31 '17 at 22:35
  • @Bergi Or, you might just say that there was an error in their docs and they fixed it. – Scott Marcus Dec 31 '17 at 23:06
  • 1
    up'ed. The first paragraph is perhaps the most satisfyingly concise and clear I've read and is, unedited, easily applied to the never-ending variations of this same question. – radarbob Mar 29 '19 at 20:34
5

Let me explain with your code:

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

At the moment the function setTimeout() was triggered, the variable of i will be equal 1,2,3,4,5 as you expected, till the value of i increases to 6 and stop the for-looping.

   var i = 1;
   setTimeout(function() { console.log(i); }, 1000*1);
   i++;
   setTimeout(function() { console.log(i); }, 1000*2);
   i++;
   setTimeout(function() { console.log(i); }, 1000*3);
   i++;
   setTimeout(function() { console.log(i); }, 1000*4);
   i++;
   setTimeout(function() { console.log(i); }, 1000*5);
   i++;
   // Now i = 6 and stop the for-looping.

After a period of time, the callback of timeout will be triggered, and do console log value of i. Have a look above, as I said, the value of i was 6 already.

    console.log(i) // i = 6 already.
    console.log(i) // i = 6 already.
    console.log(i) // i = 6 already.
    console.log(i) // i = 6 already.
    console.log(i) // i = 6 already.

The cause is the lack of ECMAScript 5: block scope. (var i = 1;i <=5 ;i++) will create a variable that will exist in the whole function, and it can be modified by the function in the local scope or closure scope. That's the reason why we have let in ECMAScript 6.

It can be fixed easily by changing var to let:

for (let i = 1; i <= 5; i++) {
  setTimeout(function() { console.log(i); }, 1000*i);
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kai
  • 3,104
  • 2
  • 19
  • 30