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?

7
  • interestingly enough, you still get 66666 if you setTimeout with 0ms pause! Commented Dec 31, 2017 at 7: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 where you can read about it Commented Dec 31, 2017 at 8: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. Commented Dec 31, 2017 at 8:17
  • you can use setTimeout(console.log(eval(i))); Commented Dec 31, 2017 at 8: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). Commented Dec 31, 2017 at 10:03

2 Answers 2

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); }

Sign up to request clarification or add additional context in comments.

16 Comments

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.
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.
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
@HugoPerea Yes, JavaScript uses lexical scoping. Closures are a byproduct of this.
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.
|
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); } 

1 Comment

Thanks. I know how this works and all I just need help understanding the part I asked about.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.