5

I understand that let has block scope and var has functional scope. But I do not understand in this case, how using let will solve the problem

const arr = [1,2,3,4];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
   console.log(arr[i]) 
}, 1000);
} // Prints undefined 5 times

const arr = [1,2,3,4];
for (let i = 0; i < arr.length; i++) {
setTimeout(function() {
   console.log(arr[i]) 
}, 1000);
} // Prints all the values correctly
Krishna
  • 137
  • 1
  • 2
  • 8

3 Answers3

4

This is all related to the scope of the variable. Let's try to wrap both the pieces into functions, and observe the output:

function test() {
  // `i` will be declared here, making it a non-for-loop scoped variable
  const arr = [1, 2, 3, 4];
  for (var i = 0; i < arr.length; i++) {
    setTimeout(function() {
      console.log(arr[i])
    }, 1000);
  } // Prints undefined 5 times
}

test();

So in the first case, i will be hoisted, and because of the asynchronous nature of setTimeout, i will immediately become 4 as the loop ends without waiting. This will make arr[i] to point to an undefined element in the array.

In the second case, i is not hoisted, and has scoped access to each iteration of the loop, making i accurately available to console.log statement. Thus the results are as per the expectations:

function test() {
  const arr = [1, 2, 3, 4];
  for (let i = 0; i < arr.length; i++) {
    setTimeout(function() {
      console.log(arr[i])
    }, 1000);
  } // Prints all the values correctly

}

test();
31piy
  • 23,323
  • 6
  • 47
  • 67
  • 1
    I still don't get it though. Is `let` scoped around `for`, or inside `for`? Is there four different `i`, or one? If one, how is it different from `var` situation (presumably the one `i` would still be captured in closure, just like with `var`)? If four, why does `i++` work (i.e. how does both old `i` and new `i` get accessed)? If anyone has relevant passages of ES2018, that would be amazeballs. (EDIT: the proposed duplicate has the answer.) – Amadan Dec 28 '17 at 06:33
  • Umm sorry I didn't get it. Can please you explain what do you mean by *hoisted*? – Souvik Ghosh Dec 28 '17 at 06:46
  • `In the second case, i is not hoisted, and has scoped access to each iteration of the loop, making i accurately available to console.log statement.` That is right but the execution of loop won't stop for `setTimeout`.In this case also the loop will finish and if you log the value of `i` outside settimeout, you will see it's value `i` is set to `length-1` much before the `settimeout` have started execution. So the question is how `settimeout` get the value of `i` even when its value is set `length-1` – brk Dec 28 '17 at 06:55
  • In the second example, the binding of `i` changes for each iteration, making an impression of having separate copies of `i`. Please go through the original post (for which this question was marked as duplicate) to clarify more. :) – 31piy Dec 28 '17 at 06:58
  • @Amadan `let` variable declared in a for loop control are scoped **inside** the for loop body. Under special rules set out in the ECMAscript standard, this "inside" scoping does not require the body to be a block statement - a body comprising a single statement without curly braces is treated as if it were a block statement within curly braces - for the purpose of creating a new block level lexical environment record for each iteration. – traktor Dec 28 '17 at 08:06
  • @traktor53 I understand that about blocks; my problem was mainly what scope is `i++` in (and getting a reference to the spec paragraph that clarifies it would be awesome). If `i++` is inside the scope, then it should be using the same variable in each iteration, as `++` modifies an existing variable; if `i++` is outside the scope, then `i` should be unknown; but neither of these is what is actually happening. Looking at babel transpiled version I can see what's happening (an outer-scope `i` getting passed as value into the inner-scope `i`), but I don't know how it's explained in the spec. – Amadan Jan 10 '18 at 01:30
  • 1
    @Amadan In ES6+ `i` is inside the block, and a new lexical environment is created for each iteration. Under "_IterationStatement_ **: for (** _LexicalDeclaration Expression_ **;** _Expression_ **)** _Statement_" in section 13.7.4.7 of the **ECMA2017** edition, a loop environment is created in step 2 and set in place in step 7. Per section 13.7.4.8, the loop environment is copied before the first loop iteration (step 2) and copied again in its existing state at the end of the loop, before the `++i' increment (step 3.e) The previous environment is restored after the loop terminates. – traktor Jan 10 '18 at 03:15
1

First of all, the output will be four times and not five times(as mentioned in your comment). I pasted your code in Babel REPL and this is what I got,

"use strict";

var arr = [1, 2, 3, 4];

var _loop = function _loop(i) {
setTimeout(function () {
   console.log(arr[i]);
}, 1000);
};

for (var i = 0; i < arr.length; i++) {
_loop(i);
}

Do you see how let works internally now? :-)

0

You can still use var for setTimeout. You can use an immediately-invoked function expression (IIFE) to create a closure around setTimeout such that the value of i is recognised by the setTimeout function.

const arr = [1,2,3,4];
for (var i = 0; i < arr.length; i++) {
(function(i){
setTimeout(function() {
   console.log(arr[i]) 
}, 1000)})(i);
}
Ankit Agarwal
  • 30,378
  • 5
  • 37
  • 62