4

I was going through the basics of javascript on freecodecamp just to refresh my memory and when I got to ES6 and the explanation of the differences between var and let, one of the examples gave me (and my colleagues) a headache.

'use strict';
let printNumTwo;
for (let i = 0; i < 3; i++) {
    if (i === 2) {
        printNumTwo = function() {
            return i;
        };
    }
}

console.log(printNumTwo());
// returns 2

console.log(i);
// returns "i is not defined"

I was expecting the printNumTwo function to return undefined, thinking that by the time it was called the variable i did not exist. One of my colleagues said that when the function expression was assigned to the variable, the i got a value of 2 so when you call the function it will always return 2.

To test this theory, we modified the original example to this:

'use strict';
let printNumTwo;
for (let i = 0; i < 3; i++) {
    if (i === 2) {
        printNumTwo = function() {
            return i;
        };
        
        i++;
    }
}

console.log(printNumTwo());
// returns 3

console.log(i);
// returns "i is not defined"

To everyone's surprise calling the function after the for loop returns 3 instead of 2 or the originally expected undefined.

Can anyone please shed some light on why is this behavior? What really happens when you assign a function expression to a variable or when you call such one?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
spartan117
  • 43
  • 3
  • 1
    You need to read about the closures, each function when you define it hold reference to enclosing environment, and it have access to every variable outside of function event when the scope if the variable ends. – jcubic Sep 04 '19 at 12:57
  • Why would you expect a different behavior? `let` makes `i` available in the whole enclosing block, the two outputs are easily predictable. – briosheje Sep 04 '19 at 12:57
  • To avoid confusion with the loop boundary, try replacing the standalone `i++;` with `i = 42;`. Then `printNumTwo` will return 42, not 3. – Scott Sauyet Sep 04 '19 at 13:28
  • 2
    This doesn't have much to do with function expressions - it just forms a normal closure. What matters for understanding the behaviour of your code is [how block scoping with `let` works in loops](https://stackoverflow.com/q/30899612/1048572) – Bergi Sep 04 '19 at 13:41
  • @FZs why do you think this was not an ES6 question? – Bergi Sep 04 '19 at 15:21
  • @Bergi I've removed the tag because the tag description says: *'Only use this tag where the question specifically relates to new features or technical changes provided in ECMAScript 2015'*, and the question didn't look like it *specifically related to ES6*... Am not I right? – FZs Sep 04 '19 at 15:43
  • @FZs `let` and `for` loop block scopes were introduced in ES6. Without them, this question would be a completely different one. – Bergi Sep 04 '19 at 16:11

4 Answers4

5

You are making and using closures. A closure is a function, plus the environment in which it was declared. When you write this line of code:

printNumTwo = function() {
  return i;
};

That function has a reference to the i variable. For as long as this function exists, that variable will not be garbage collected and can continue to be referenced by this function. It's not saving a snapshot of what the value was, but saving a reference to the actual variable. If that variable changes, as in your second example, then the reference sees that modified value.

Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
2

I don't know if an ASCII visualization will help. This is how I think about it. Note that I extended the loop to (i < 5); that extra iteration might clarify things.

+-------------+
| printNumTwo |                       --------------------------
+------+------+                       Loop starts  
       |                              for (let i = 0; i < 5; i++) 
       |                              --------------------------
       |         +-------------+ \    
       |         |             |  |
       |         |    i = 0    |  |-- discarded
       |         |             |  |
       |         +-------------+ /
       |
       |         +-------------+ \
       |         |     i++     |  |
       |         |  // i = 1   |  |-- discarded
       |         |             |  |
       |         +-------------+ /
       |
       |         +-------------+ \
       |         |     i++     |  |
       +-------> |  // i = 2   |  |-- kept since `printNumTwo`
                 | printNumTwo |  |   still has a reference
                 |     i++     |  |
                 +-------------+ /

                 +-------------+ \
                 |     i++     |  |
                 |  // i = 4   |  |-- discarded
                 |             |  |
                 +-------------+ /
                                      --------------------------
                       i++
                       i < 5: false   Loop ends
                                      `i` now out of scope
                                      --------------------------

                                      > printNumTwo() //=> 3
                                      > i      // not defined
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • There is no scope with `i = 3`, since the loop already ends after the conditional `i++` has incremented `i` above the end condition – Bergi Sep 04 '19 at 13:39
  • @Bergi, note the intro text. I thought looping only until `i < 3` might cause confusion. Obviously changing the loop boundary caused its own confusion! – Scott Sauyet Sep 04 '19 at 14:03
  • I still mean it. After the iteration with `i = 2`, first the `i++` in the loop body increments `i` to `3`, then the `i++` in the loop head increments `i` further to `4`, where the loop ends. If you did use `i < 5`, you'd get another iteration with `i = 4`. – Bergi Sep 04 '19 at 14:15
  • @bergi: Oh, damn, of course. Sorry. That's going to be hard to explain in the diagram. – Scott Sauyet Sep 04 '19 at 15:33
  • 1
    @bergi: Nice! This is cleaner than [my attempt](https://github.com/CrossEye/temp/blob/master/so57788803.md), which added two more columns and labels for the parts of the for expression -- too ugly. Thanks for the cleanup! – Scott Sauyet Sep 04 '19 at 17:06
1

Based on Nicholas Tower's answer I wanted to extend the original example to show the kept reference for variable i even if it is a value type of variable with a setter function called setNumber:

(function() {
  'use strict';

  let printNumber,
      setNumber;

  for (let i = 0; i < 3; i++) {
    if (i === 2) {
      printNumber = function() {
        return i;
      };

      setNumber = function(value) {
        i = value;
      }
    }
  }

  console.log('should be 2 - ', printNumber()); // from your example

  setNumber(17); // changing the original i variable's value
  console.log('should be 17 - ', printNumber()); // printing the changed value

  console.log(i); // Uncaught ReferenceError: i is not defined
})();

So basically you have a setter function as well for variable i called setNumber which changes the original i. The function printNumber represents the changed variable's value in the above example.

norbitrial
  • 14,716
  • 7
  • 32
  • 59
0

It does make sense of-course. the printNumTwo is instantiated when i=2 then you i++ so the i = 3.

KLTR
  • 1,263
  • 2
  • 14
  • 37
  • 2
    Then why doesn't `i++` in the loop head affect `i` in the closure? How do you think "*is instantiated when `i=2`*" does work exactly? – Bergi Sep 04 '19 at 13:43