2

In below snippet, the function passed to setTimeout forms a closure with the variable i which is present in the script scope. So the function contains a reference to variable i. The value of i is updated to 5 before i is logged to console. And the output is: 5 5 5 5 5 (with newlines)

script.js:

let i;
for (i = 0; i < 5; ++i) {
    setTimeout(() => {
        console.log(i);
    }, i * 1000);
}

The above part is clear to me. But when I declare variable i inside for loop, then i is block scoped to the for loop.

for (let i = 0; i < 5; ++i) {
    setTimeout(() => {
        console.log(i);
    }, i * 1000);
}

In this code snippet, I expect the output to be same as output of first snippet, because the closure contains reference to the variable i and the value at that reference should be updated to 5 before the value is logged to console. But the actual output is: 0 1 2 3 4 (with newlines)

The question is, why the code in second snippet behaves this way?

It it because:

  1. A new copy of variable i is created in memory for each iteration of for loop and previous copy is garbage collected? I'm not sure how memory management works for loops.
  2. Instead of storing reference of i, the value of i is stored in closure? I don't think this is the case.
  3. Anything else?

Please help to clarify. Thanks!

Mohit Yadav
  • 145
  • 1
  • 12
  • 1
    You've answered your own question: "_`i` is block scoped_", naturally the behavior of the code is different when you declare a block scoped variable in a different scope. – Teemu Dec 13 '21 at 18:30
  • 1
    In the first example there's just one variable `i` that's shared by all the closures. In the second example there's a different scope for each loop iteration. – Barmar Dec 13 '21 at 18:31
  • @Teemu "the behavior of the code is different when you declare a block scoped variable in a different scope." Can you please elaborate? – Mohit Yadav Dec 13 '21 at 18:48
  • @Barmar "there's a different scope for each loop iteration". But the scope contains the reference to variable i having same memory space, right? Or is the scope entirely different having separate memory space? Do you mean that in each iteration the variable reference is changed? – Mohit Yadav Dec 13 '21 at 18:54
  • My English dictionary is not good enough to explain this in English, please take a look at [this article](http://dmitrysoshnikov.com/ecmascript/javascript-the-core-2nd-edition/#environment), I suppose that's a pretty accurate description of how the scopes work. – Teemu Dec 13 '21 at 19:14
  • In short based on the linked article: Each round of a `for` loop creates a new Environment Record, which contains also the variable values for the Record. When you exit an iteration round, the Record is pulled out of the Stack, but the later used variables are stored in a closure (you can even see the stored values at Chromium DevConsole). When the timer fires, the Record created for it is pulled from the stack and the callback is executed (timer can fire only when there's no other Records under the timerspecific Record in the Stack). – Teemu Dec 13 '21 at 19:30
  • Does this answer your question? [What's the difference between using "let" and "var"?](https://stackoverflow.com/questions/762011/whats-the-difference-between-using-let-and-var) – idmean Dec 13 '21 at 19:32
  • @Teemu The article you shared is helpful in understanding the execution environment in general but it does not explain how `let` variable behaves with `for` loops. Even if new Environment Record is created, it should keep the _reference_ to same variable having same memory location unless it is the case that new a new copy of the variable is created with each iteration. Can you help to figure out if _reference_ to `i` is changed with each iteration? I think the _reference_ changes but I'm not sure why. – Mohit Yadav Dec 14 '21 at 06:05
  • @idmean It explains the basic differences between `let` and `var` variables that I already know. I think this case is specific to inner working of `for` loops. – Mohit Yadav Dec 14 '21 at 06:09
  • "_it should keep the reference to same variable_" no, because a new Record is created, every Record has its own __block scoped__ variables. The scope of the block includes the `for` statement, it's not just the codeblock after the statement. – Teemu Dec 14 '21 at 06:13
  • @Teemu The Environment Record contains **reference** to the original variable or its copy? As per the article that you shared, it contains the reference. _"every Record has its own block scoped variables"_ Do you mean that it stores the variable by value and not by reference? – Mohit Yadav Dec 14 '21 at 06:23
  • Yes, everything in JS is stored/passed by value, even objects can be referred only with a reference, which itself is a value. You can see the Foo envRec table in the article, it shows the variable name and the value. – Teemu Dec 14 '21 at 06:28
  • @Teemu Isn't reference to `i` stored in case of first snippet, resulting in output `5 5 5 5 5`? I also checked in case of `var` by updating `window.i` to some other value at runtime and the output changes. I think this proves that the reference to variable is stored instead of its value. You can check the example [here](https://codepen.io/Mohityadav7/pen/PoJWbQK?editors=0011) – Mohit Yadav Dec 14 '21 at 06:40
  • Again, there's no reference to a variable, the values are stored into the lookup table the Records contain. If the currently executing Record doesn't contain the variable, then the parent is checked, this happens until the parent is not found, then you'll get an error. Like the Soshnikov's article says: "_an environment record (__an actual storage table which maps identifiers to values__)_". – Teemu Dec 14 '21 at 08:20

1 Answers1

2

Your question mentions a few implementation details. Yet, these implementation details don't matter when answering your question.

You are indeed right, that there is some specific behavior of the for-loop occurring. Let's look at the ECMAScript 2021 standard here:

When your for-loop with let is evaluated, these steps are performed first:

enter image description here

Steps 4, 9 and 10 are particularly interesting to us. Step 4 gathers all const and let variables declared in the for-loop.

Step 9 sets perIterationLets to the let variables (if there are let variables) or to the empty list otherwise. Step 10 then calls ForBodyEvaluation to actually run the loop:

enter image description here

Here, let's focus on 3.e and take a closer look at CreatePerIterationEnvironment:

enter image description here

Carefully following the invocation hierarchy, we note that perIterationBindings is the list perIterationLets of let variables we gathered before.

In lines e.i, e.ii, e.iii, we now create a new binding (think of this as the memory location where the variables lives) and copy the value of the variable with the same name from the previous iteration into it. It is a different new variable though, even if it has the same name!

Thus if a closure inside a loop body captures the current context, the loop variables will not change, because each loop body iteration has its own current context.

idmean
  • 14,540
  • 9
  • 54
  • 83