2

In the mdn docs it gives an example in which functions are initialized in the for loop initialization block

for (
  let i = 0, getI = () => i, incrementI = () => i++;
  getI() < 3;
  incrementI()
) {
  console.log(i);
}
// Logs 0, 0, 0

It gives the following explanation of why this occurs:

This logs "0, 0, 0", because the i variable in each loop evaluation is actually a separate variable, but getI and incrementI both read and write the initial binding of i, not what was subsequently declared.

For each iteration we get a new i in the for loop body. The i variable in the initialization block is also in the scope of the for loop body.

I am hoping someone can explain this a bit better than what mdn did, I am having a bit of a hard time grasping what is going on here since there is a closure here, the i variable belongs to the scope of the for loop body, and when I add a debugger statement to getI or incrementI everything looks as one might expect, just inside the for loop body it doesn't seem to see it and it logs 0 each iteration.

dbzx10299
  • 722
  • 2
  • 14
  • This is the very nature of the `arrow functions`. If you replace those with `function expressions` it is not going to work that way. – Cody Tookode Jun 09 '23 at 07:28
  • 2
    @AlPo this has nothing to do with function expressions vs arrow functions. The code works the same with function expressions as well. It's more to do with how the functions update the initial binding of `i` created only, not each one created per-iteration – Nick Parsons Jun 09 '23 at 07:32
  • @AlPo I'm not sure what difference between function expressions vs arrow functions you're referring to then that's making this behave differently. See https://jsfiddle.net/w8p4o3ft/ it outputs `0`, `0`, `0`, same as the above code – Nick Parsons Jun 09 '23 at 07:47
  • @NickParsons =) it's not fair! You added the return in condition – Cody Tookode Jun 09 '23 at 07:50
  • 1
    @AlPo I wouldn't call it unfair, it's just the equivalent version of the arrow function., since arrow function's have implicit returns if they don't have a body. Your code would be more accurate if the original code was `let i = 0, getI = () => {i}, incrementI = () => {i++;}` :P – Nick Parsons Jun 09 '23 at 07:53
  • 1
    You are right! I have to admit I didn't understand how it actually occured. Thank's for the explanation. – Cody Tookode Jun 09 '23 at 08:18
  • I was looking for a simple explanation and came up with the wrong one for myself. Because `getI` and `incrementI` evaluated once before the loop begins, `i` inside `getI` and `indcrementI` is incrementing on each loop evaluation. See https://jsfiddle.net/bva6kp3d/ – Cody Tookode Jun 09 '23 at 09:43
  • Did you read [Explanation of `let` and block scoping with for loops](https://stackoverflow.com/a/30900289/1048572)? – Bergi Jun 09 '23 at 10:47
  • Yes I did, very clear explanation. So the reason this is happening is because the variables from the new scope are actually created based off of the value within the scope body from the previous iteration. I have two questions regarding your example, what happens when the condition evaluates to false, do the nested scopes start popping, and is a new scope created for the last iteration that evaluates to false? – dbzx10299 Jun 09 '23 at 16:36
  • The scopes are not nested, they are sibling scopes. And yes, a new scope is created for the iteration where the condition evaluates to `false`, even if the loop body is no longer executed in that scope. – Bergi Jun 09 '23 at 17:15

2 Answers2

2

Each iteration of your loop creates a new scope that has its own binding for i. In addition, before your loop begins iterating, a separate scope is created that initializes your i, getI, and incrementI variables. For each iteration that follows, the new bindings that are created in the new scopes are initialized to the values of the variables created in the previous scope created.

Your functions are initially created in the initial scope before your loop beings executing and have visibility over the i created in that initial scope, and so that's the version of i that your functions update/read, and is also why the loop is eventually able to terminate.

Your for-loop body however has access to the i defined in the new scope created at that particular iteration of the loop that’s executing, which is 0 as the incrementer expression does not update that particular i binding, it only updates the initial binding created at the beginning of all the iterations.

To put things into perspective, you could think of your loop flattening out to something like below for the first two iterations, where i_0 and i_1 represent your i variable at different iteration stages:

// Initial scope, created before iterations occur
let i, incrementI, getI;
i = 0;
incrementI = () => i++;
getI = () => i;

// First iteration scope (new bindings for `i`, `incrementI` and `getI` are created. 
let i_0, incrementI_0, getI_0;
i_0 = i;
incrementI_0 = incrementI;
getI_0 = getI;

if(getI_0() < 3)
  console.log(i_0);


// Second iteration scope (new bindings for `i`, `incrementI` and `getI` are created)
let i_1, incrementI_1, getI_1; // new declarations are created for new iteration
i_1 = i_0; // 0, as i_0 is 0
incrementI_1 = incrementI_0;
getI_1 = getI_0;

incrementI_1(); // increments i_0, not i_1. i_0 is now 1

if(getI_1() < 3) // checks i_0 (now 1) is less than 3.
  console.log(i_1); // logs i_1, still 0
Nick Parsons
  • 45,728
  • 6
  • 46
  • 64
  • But, but intialization `evaluated once before the loop begins`, so there is no `let i_1, incrementI_1, getI_1;` on the second iteration. You call `getI_1` from condition and incrementI_1 from afterthought on second iteration. Right? – Cody Tookode Jun 09 '23 at 07:35
  • 1
    @AlPo Because `i`, `incrementI` and `getI` are all defined with `let`, a new binding is created for them at each iteration, so in effect, there are new variables being created for each iteration (which I've represented with `_0`, `_1`, etc.) and they're initialised to the values from the previous iteration that occurred ([source](https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-forbodyevaluation)). – Nick Parsons Jun 09 '23 at 08:07
  • 1
    Aha! Still trying to understand the `[Yield, Await, Return]` semantics with different signs `[?, +, ~]` from that source above. An actual revelation for me, thanks again. – Cody Tookode Jun 09 '23 at 08:40
  • 1
    @AlPo No worries, yeah, trying to wrap your head around the strange syntax the spec uses is a bit tricky. I recommend checking out these articles by Marja Hölttä (one of the engineers working on V8) if you want to get a better understanding: [Understanding the ECMAScript spec](https://v8.dev/blog/tags/understanding-ecmascript). She covers `[Yield, Await, Return]` and `+`, `?` in [part three](https://v8.dev/blog/understanding-ecmascript-part-3#productions-and-shorthands) of this section (although I've read it some time ago, I've forgotten what it means now). – Nick Parsons Jun 09 '23 at 08:46
  • 2
    "*Each iteration of your loop has its own binding for `i`*" - actually there's 5 different scopes, 1 before the loop and 4 during the iterations – Bergi Jun 09 '23 at 10:55
  • @Bergi Thanks, I've tried to take a further look at the spec and updated my answer based on my understanding. I can see that multiple scopes are being created. I'm struggling to see the need for the ER created in [ForBodyEvaluation](https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-forbodyevaluation) point 2, as `loopEnv` created when evaluating the [ForStatement](https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forloopevaluation) (`for ( LexicalDeclaration ...`) seems to hold the same bindings – Nick Parsons Jun 11 '23 at 04:07
  • 1
    @NickParsons Tbh I have no idea. Step 3.e should suffice. At first I thought maybe they wanted to avoid special-casing the first iteration to contain the evaluation of the declaration, but then again they already *do* special-case the first iteration by not evaluating the *increment* expression. Still, maybe there's an advantage (like for garbage collection? or optimisation by merging each iteration scope with a block scope?) if the declaration initialiser is evaluated in a separate scope. – Bergi Jun 11 '23 at 04:34
  • @Bergi interesting ideas, thanks for having a look – Nick Parsons Jun 12 '23 at 01:24
0

the i variable belongs to the scope of the for loop body

Indeed, but the thing is (as mdn wrote it):

This is because getI is not re-evaluated on each iteration — rather, the function is created once and closes over the i variable, which refers to the variable declared when the loop was first initialized. Subsequent updates to the value of i actually create new variables called i, which getI does not see.

It's a quote but i could honestly not explain it better. You could maybe see it as : 3 i variable are created and your getI() is only binded to the first one basically.

tlvi38
  • 281
  • 1
  • 6