4

Given the following simplified code, why do I get a ReferenceError?

    function foo(n){
        for (let i = 0; i < n; i++) {       
            console.log(i);
            let i = 10;
        }
    }

    foo(4);

// OUTPUT: ReferenceError: i is not defined

let variables should be hoisted as all other types of the declaration. Hence, in this code i is declared twice. First, in the initialization close and second in the for loop itself.

Why does this output i is not defined instead of Identifier 'i' has already been declared.

How does JS read the code above?

Engineer
  • 8,529
  • 7
  • 65
  • 105
leonardofed
  • 936
  • 2
  • 9
  • 24
  • 2
    This is a good question imo. If we removed `let i = 10`, it will works. And to my understanding, `let` don't `hoist` for each and every `scope` of loop, so I would expect to print `0,1,2,3` instead – Isaac Jun 24 '18 at 13:37

3 Answers3

3

What happens underneath is that JavaScript sees two different level of block scope inside the same for-loop.

The first is declared during the initialization clause of the loop (let i = 0;) the second inside the loop itself.

This is what happens when you run the code for the first time:

  • let in the initialization clause: i = 0;
  • let inside the for-loop: i is set to undefined

enter image description here

When the for loop hits console.log(i) it finds the declaration of let i inside the loop which overrides the previously declared and assigned let i (in the initialization clause.

Since per hoisting, let and const are declared but not initialized (as undefined) it throws a ReferenceError.

leonardofed
  • 936
  • 2
  • 9
  • 24
  • As far as I'm concerned, this is a bug - a browser bug at least. I suffered a wasted hour today because I was not warned that I was using the same variable name twice. The developer MUST be warned about this. – Engineer Jan 07 '19 at 15:46
2

let variables should be hoisted as all other types of the declaration.

let variables are not hoisted, unlike var variables and function and class declarations. const variables are the same.

The variable does not come into existence until you reach the line where it is declared.

However, you cannot access the variable in the relevant scope until you reach the declaration. This is, somewhat unhelpfully, known as the temporal dead zone. (Useful 2ality blog)

This is helpful, because accessing a variable before it is declared is almost always an error. Or, at the very least, it is confusing and will lead to errors when you refactor.

function foo(n){
    for (let i = 0; i < n; i++) {       
        // i cannot be accessed
        let i = 10;
        // i is set to 10
    }
}

foo(4);

As for why it isn't an "identifier already declared" error, the scope of the variable declared in the for loop header is a separate variable to the one declared in the block. There are essentially three scopes in this code: the outer one, the one begun in the for loop header, and the one begun in the block surrounded with {}.

lonesomeday
  • 233,373
  • 50
  • 316
  • 318
  • 1
    from the article: > The rationale here is as follows: foo is not undeclared, it is uninitialized. You should be aware of its existence, but aren’t. Therefore, being warned seems desirable. const and let are "hoisted" like var and functions. Unlike the others, they aren't initialized. So you cannot access the variable in the relevant scope until you reach the initialization (and not the declaration). – leonardofed Jun 24 '18 at 14:57
-2

Accroding to MDN:

function test(){
  var foo = 33;
  if (true) {
   let foo = (foo + 55); // ReferenceError
  }
}
test();

Due to lexical scoping, the identifier "foo" inside the expression (foo + 55) evaluates to the if block's foo, and not the overlying variable foo with the value of 33. In that very line, the if block's "foo" has already been created in the lexical environment, but has not yet reached (and terminated) its initialization (which is part of the statement itself): it's still in the temporal dead zone.

This means that if the loop variable is "read" in your example, (console.log in your case, you cant assign it to a same name var, as its in the "dead zone"

MDN ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let (under The temporal dead zone)

Itamar
  • 1,601
  • 1
  • 10
  • 23
  • Nope. You can simply remove `let i = 10`, you will see it prints `0,1,2,3` – Isaac Jun 24 '18 at 13:39
  • 2
    The logs are created in each loop, not after the loop ends. You can confirm this by stepping through the code in a debugger. – Heretic Monkey Jun 24 '18 at 13:48
  • 1
    `[...]Now in next loop iteration, now i is defined.[...]` That's not true, the `i` will always be defined at the time of the evaluation of `let i`, so for every iteration of the loop the `i` in the loop block will not be available until `let i` is reached. – t.niese Jun 24 '18 at 13:52
  • 1
    A better example might be the second one under https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#Another_example_of_temporal_dead_zone_combined_with_lexical_scoping since the lexical scope of the `i` in the `for` initialiizer is essentially thrown away for the scope of `let i = 10;` in the loop's scope. – Heretic Monkey Jun 24 '18 at 13:53
  • But this does still not really explain to which scope the `let i` of `for (let i = 0; i < n; i++) {` belongs to. It is neither the scope of `foo` nor the loop body. – t.niese Jun 24 '18 at 13:55
  • @MikeMcCaughan thats in the new "version" :). i have changed the answer – Itamar Jun 24 '18 at 13:55