getify / You-Dont-Know-JS

A book series on JavaScript. @YDKJS on twitter.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Scope and Closures - Ch. 5 - Loops - `for` initializer is not declared in loop body

witemple-msft opened this issue · comments

(https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/ch5.md#loops)

Please type "I already searched for this issue":
I already searched for this issue.

Edition: (1st or 2nd)
2nd

Book Title:
Scope and Closures

Chapter:
5

Section Title:
Loops

Problem:

There's a technical error in the section about the traditional for-loop construction and how it interacts with let and const, when considering the following loop:

for (let i = 0; i < 3; i++) {
    let value = i * 10;
    console.log(`${ i }: ${ value }`);
}

The section says "consider what scope i is in. It might seem like it would be in the outer (in this case, global) scope, but it's not. It's in the scope of for-loop body, just like value is..." However, it's actually neither. It does not become part of the outer (global) scope, nor is it declared in the loop body, but rather in an intermediate scope. Consider:

for (let i = 0; i < 3; i++) {
    let i = "foo";
    console.log(i);
}

This is valid and the inner let simply shadows the let-binding in the for-initializer. If the behavior was as described in the chapter, then this would yield a SyntaxError for redeclaring the let binding. The construction is more or less equivalent to this instead:

{
  let i = 0;
  while (i < 3) {
    {
      let i = "foo";
      console.log(i);
    }

    // loop control
    i++;
  }
}

The for-initializer, condition, and iteration step get their own little block scope. The following sections expound on the idea of the "conceptual variable" that is created by the looping construct with for (let i; ...) and for (const i; ...), and describe a level of meta-transformation that doesn't actually occur. Really, it's just that the value is in an intermediate scope, and const doesn't work as a variable to store an iterating value because const by definition cannot be reassigned, which is a conclusion that is ultimately reached, but it gets there by quite convoluted logic that comes from this one relatively small technical mistake.

Even the following mind-numbing abuse of the intermediate scope is fine with the engine:

let i = 0;

// Capture outer `i`
function get() { return i; }
function inc() { i++; }

// `get2` captures intermediate `i`
for (const i = 3, get2 = () => i; get() < i; inc()) {
  const i = get(); // doesn't redeclare or reassign i
  console.log(i * 10, get2());
}
// 0 3
// 10 3
// 20 3

There's a technical error...

I'm well aware of these sorts of nuances of the specifications and the corner cases of behavior. I intentionally omitted discussion of it here because I do not think JS developers will hardly ever need to know such things.

The chances of coming into a quirk of behavior where it shows up, such as shadowing (as you showed) is exceedingly rare in practice, and many linter configs protect against it anyway. In fact, I bet the only folks who really ever get that deep with spec nuances are JS engine implementors or tool authors like Babel/etc. That's a really small audience.

My strategy in these books is to defer more nuanced details from the main text so as to not distract the typical reader down too many rabbit holes. Then, in some cases, I will come back later and fill in the "well, actually..." nuances in an appendix, for those who are curious and want to dive deeper.

For example, I cover two examples of these "implied" / "intermediate" scopes in this section of appendix A: https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/apA.md#implied-scopes

I could have mentioned the for-loop initializer intermediate scope in that appendix section. But I chose not to since I felt it was already deep enough in the weeds.

A book can never cover everything you could possibly say on a topic, and still serve all its readers well. Choices have to be made.

I chose to omit the specific nuance you've brought up. I don't consider that a technical error. But you're entitled to that opinion.

Closing for now. Feel free to add further discussion if you see fit.