getify / You-Dont-Know-JS

A book series on JavaScript. @YDKJS on twitter.
Other
177.97k stars 33.41k forks source link

let declaration in a for loop creates a new variable for each iteration of the loop #1714

Closed juniorforlife closed 3 years ago

juniorforlife commented 3 years ago

"I already searched for this issue"

Edition: 2nd Book Title: You Don't Know JS Yet: Scope & Closures - 2nd Edition Chapter: 7 Using Closures Section Title: Live Link, Not a Snapshot

Question: I have a question regarding your explanation on this snippet

var keeps = [];

for (let i = 0; i < 3; i++) {
    keeps[i] = function keepEachI(){
        return i;
    };
}
keeps[0]();   // 0
keeps[1]();   // 1
keeps[2]();   // 2

You mentioned that let declaration in a for loop creates a new variable for each iteration of the loop Would this conflict with the default behavior of for-loop in JS? I mean the first expression of for-loop is evaluated only once and before the loops run right?

getify commented 3 years ago

Good question, nuanced answer. The thing that only happens once is the initialization: the = 0 part. Even in pre-ES6 JS, in a loop like this:

for (var i = 0; .. ) { .. }

The thing that made the var i happen "only once" wasn't the for loop mechanics, it was the overriding mechanics of how var is "hoisted" inside the function declaration.

So, once let was introduced, they decided to extend the nuanced behavior of the for loop to actually treat the let as being part of each iteration's setup.

The other options could have been to hoist the let to outside the loop (but that didn't make sense) or to register the let just once for a scope that encompassed just outer bounds of the loop itself. The latter of these would have been "fine", but it was less helpful (in terms of solving the common pain points of closure inside loops), so they opted for the more helpful behavior of scoping per iteration.

juniorforlife commented 3 years ago

Awesomeeeeeeeee! Thank you so much Kyle. I've read so many stackoverflow and blog posts and no one has ever come close to answer it in such a clarified and sensible way like you. Thumbs up!!!

juniorforlife commented 3 years ago

Sorry for reopening this issue. I did an experiment as below:

var previous;
for(let i = {x: 0}; i.x < 5; i.x++){
  console.log(previous === i);
  previous = i;
}

The result is false, (4) true. At first, I thought the result would discard your statement let declaration in a for loop creates a new variable for each iteration of the loop because obviously we can see that it's the same object. However, after taking a moment to think, I realized that you said let creates a new variable, NOT a new value. So this is my guess about what happens:

  1. before the for loop starts, creates a new i
  2. i = {x: 0} is executed ONCE
  3. check i.x < 5 -> true
  4. execute the loop body
  5. execute i.x++ which has nothing todo with creating a new value.
  6. check i.x < 5 -> true
  7. enter loop body, create a new i, assigns it to the same object created in step 2 which is now looking like this {x: 1} then execute whatever is inside the loop body repeats until the terminate condition is true;

So in this case, capturing the value of i.x per-iteration like in the original snippet won't work because we use object, not primitive value. I guess the only way to do this is to deep clone i (eg: const j = JSON.parse(JSON.stringify(i));)

I'd greatly appreciate it if you could verify my understanding. Thank you!

Edited: I also wonder does this mean creating variables won't cost as much memory as creating values?

getify commented 3 years ago

This isn't a perfect translation, but it should help illustrate kind of what the engine is doing under the covers:

for (let i = 0; i < 5; i++) {
   console.log(i);
}

// is treated sorta like:

{
   let $i = 0;
   for ( ; $i < 5; $i++) {
      let i = $i;
      // ****** loop body ******
      console.log(i);
      // ************************
      $i = i;  // to pick up on any changes made to `i` in the original loop body
   }
}

I show it this way to highlight this specific line (again, illustrative, not actual): let i = $i. The assignment there is basically a per-iteration initialization to whatever the value of the incrementer ($i) was at the end of the previous loop (if any).

If the value being assigned is not a primitive like a number, but is instead an object (as in your second example), then the assignment is by reference-copy, which means that $i and the loop's internal i both point at the same object (the both have copies of the reference to it).

If you want to preserve a value in a closure, it has to be assigned to a variable that exists in the closure, because closure is only over variables, not values. In your second example case, the x property isn't involved in the closure in anyway -- there's no variable in the closure preserving its value -- it's just part of the parent object value that's being held by the closed-over variable (i aka $i).

So yes, I think your typed out steps are roughly accurate. But the more important point is to focus on the fact that closure is only over variables, so "capturing a value in a closure" requires that value to actually be assigned to a variable that participates in the closure. A sub-property doesn't satisfy that condition, which is why it won't be captured/preserved in the way one might expect.

juniorforlife commented 3 years ago

Do you mind sharing how you know about these details and more importantly, how you verify your understanding? There're many widely misunderstandings like hoisting moves up variable and function declarations (it's actually the creation phase of the execution context) or closure closes over values (instead it closes over variables).

I think it'd be super helpful for us young developers to acquire the research and verify skills you have. Thank you!

getify commented 3 years ago

I wish there was a super straightforward way of that. I have acquired and refined my understanding of these things over a decade of reading the spec, talking to those who work on TC39 and trying to clarify, testing the code myself, and even reading source of JS engines. It's a very messy process. That's part of what motivated me to leave the summaries behind me in the form of these books.

juniorforlife commented 3 years ago

Knowing what it took you to understand JS at this level, I can comfortably say I don't know Javascript ... yet. I truly appreciate what you've done for the tech/JS community. Thank you!