brk
brk

Reputation: 50346

What is the purpose of let hoisting in ES6?

I understand that let will be hoisted to top of the block, but accessing it before initializing will throw ReferenceErrordue being in to Temporal Dead Zone

For example:

console.log(x);   // Will throw Reference Error
let x = 'some value';

But a snippet like this will run without error:

foo(); // alerts foo;
function foo(){    // foo will be hoisted 
  alert("foo");
} 

My question

What is purpose of let getting hoisted to top when it will throw an error on accessing? Also do var also suffer from TDZ,I know when it will throw undefined but is it because of TDZ?

Upvotes: 6

Views: 1296

Answers (5)

T.J. Crowder
T.J. Crowder

Reputation: 1075875

What is purpose of let getting hoisted to top when it will throw an error on accessing?

It's so we can have block scope, which is fairly understandable concept, without having the block equivalent of var hoisting, which is a traditional source of bugs and misunderstandings.

Consider the inside of this block:

{
    let a = 1;
    console.log(a);
    let b = 2;
    console.log(a, b);
    let c = 3;
    console.log(a, b, c);
}

The designers had three main choices here:

  1. Have block scope but with all the declarations hoisted to the top and accessible (like var is in functions); or
  2. Don't have block scope, and instead have a new scope start with every let, const, class, etc.; or
  3. Have block scope, with hoisting (or what I call "half-hoisting"), where the declarations are hoisted, but the identifiers they declare are inaccessible until they're reached in the code

Option 1 leaves us open to the same kinds of bugs we have with var hoisting. Option 2 is way more complicated for people to understand, and more work for JavaScript engines to do (details below if you want them). Option 3 hits the sweet spot: Block scope is easy to understand and implement, and the TDZ prevents bugs like those caused by var hoisting.

Also do var also suffer from TDZ,I know when it will throw undefined but is it because of TDZ?

No, var declarations have no TDZ. undefined isn't thrown, it's just the value a variable has when it's declared but not set to anything else (yet). var declarations are hoisted to the top of the function or global environment and fully accessible in that scope, even before the var line is reached.


It may help to understand how identifier resolution is handled in JavaScript:

The specification defines it in terms of something called a lexical environment, which contains an environment record, which contains information about the variables, constants, function parameters (if relevant), class declarations, etc. for the current context. (A context is a specific execution of a scope. That is, if we have a function called example, the body of example defines a new scope; every time we call example, there are new variables, etc., for that scope — that's the context.)

The information about an identifier (variable, etc.), is called a binding. It contains the identifier's name, its current value, and some other information about it (like whether it's mutable or immutable, whether it's accessible [yet], and so on).

When code execution enters a new context (for instance, when a function is called, or we enter a block containing a let or similar), the JavaScript engine creates* a new lexical environment object (LEO), with its environment record (envrec), and gives the LEO a link to the "outer" LEO that contains it, forming a chain. When the engine needs to look up an identifier, it looks for a binding in the envrec of the topmost LEO and, if found, uses it; if not found, looks at the next LEO in the chain, and so on until we reach the end of the chain. (You've probably guessed: The last link in the chain is for the global environment.)

The changes in ES2015 to enable block scope and let, const, etc. were basically:

  • A new LEO may be created for a block, if that block contains block-scoped declarations
  • Bindings in an LEO may be marked "inaccessible" so the TDZ can be enforced

With all that in mind, let's look at this code:

function example() {
    console.log("alpha");
    var a = 1;
    let b = 2;
    if (Math.random() < 0.5) {
        console.log("beta");
        let c = 3;
        var d = 4;
        console.log("gamma");
        let e = 5;
        console.log(a, b, c, d, e);
    }
}

When example is called, how does the engine handle that (at least, in terms of the spec)? Like this:

  1. It creates an LEO for the context of the call to example
  2. It adds bindings for a, b, and d to that LEO's envrec, all with the value undefined:
  • a is added because it's a var binding located anywhere in the function. Its "accessible" flag is set to true (because of var).
  • b is added because it's a let binding at the top level of the function; its "accessible" flag is set to false because we haven't reached the let b line yet.
  • d because it's a var binding, like a.
  1. It executes console.log("alpha").
  2. It executes a = 1, changing the value of the binding for a from undefined to 1.
  3. It executes let b, changing the b binding's "accessible" flag to true.
  4. It executes b = 2, changing the value of the binding for b from undefined to 2.
  5. It evaluates Math.random() < 0.5; let's say it's true:
  6. Because the block contains block-scoped identifiers, the engine creates a new LEO for the block, setting its "outer" LEO to the one created in Step 1.
  7. It adds bindings for c and e to that LEO's envrec, with their "accessible" flags set to false.
  8. It executes console.log("beta").
  9. It executes let c = 3, setting c's binding's "accessible" flag to true and setting its value to 3
  10. It executes d = 4.
  11. It executes console.log("gamma").
  12. It executes let e = 5, setting e's binding's "accessible" flag to true and setting its value to 5.
  13. It executes console.log(a, b, c, d, e).

Hopefully that answers:

  • Why we have let half-hoisting (to make it easy to understand scope, and to avoid having too many LEOs and envrecs, and to avoid bugs at the block level like the ones var hoisting had at the function level)
  • Why var doesn't have a TDZ (a var variable's binding's "accessible" flag is always true)

* At least, that's what they do in terms of the specification. In fact, they can do whatever they like provided it behaves as the specification defines. In fact, most engines will do things that are more efficient, utilizing a stack, etc.

Upvotes: 0

voltrevo
voltrevo

Reputation: 10459

A let variable is not hoisted. Saying a let variable is 'hoisted' is technically correct but in my opinion this use of the term is misleading. An equivalent way to describe the semantics is that you get a ReferenceError when you try to refer to it above its declaration because it doesn't exist yet; the same thing you'd get if you tried to refer to a variable that didn't exist anywhere in that block.

More information:

C++ and JavaScript both have block scoping but differ on this specific point, so we can understand this point by understanding how they behave differently. Consider this example:

#include <iostream>                                                         

int main() {
    int x = 3;

    {
        std::cout << x << std::endl;
        int x = 4;
    }

    return 0;
}

In C++ there really is no hoisting, the second x doesn't exist when the cout line runs (which prints x to the screen), but the first x still does, and so the program obediently prints 3. That's pretty confusing. We should instead consider that reference to x to be ambiguous and make it an error.

This is what happens in the analagous JavaScript code:

'use strict';                                                               

let x = 3;

(() => {
    console.log(x);
    let x = 4;
})();

In JavaScript, this problem has been fixed by 'hoisting' the second x, but making it throw a ReferenceError when accessed. As far as I know, this 'hoisting' is equivalent to making that reference to x an error due to ambiguity, as it should be.

Upvotes: -1

potatopeelings
potatopeelings

Reputation: 41085

http://www.2ality.com/2015/10/why-tdz.html explains it in a nice way and also links to https://mail.mozilla.org/pipermail/es-discuss/2012-September/024996.html which is a related discussion on the topic.

Paraphrasing the content for this question

Why is there a temporal dead zone for let?

  1. If the TDZ did not cause a reference error, and you accessed a variable before its declaration (i.e. in the TDZ) you'd (most probably) be missing a programming mistake. The TDZ causing the reference error helps you catch your programming mistake.

  2. So your next question would be - why even have a TDZ for let? Why not start the scope of a let variable when its declared? The answer is const. TDZs are for const and (poor) let got stuck with TDZ just to make it easier to switch between let and const


Also do var also suffer from TDZ, I know when it will throw undefined but is it because of TDZ?

No, var does not suffer from TDZ. It does not throw any error. It is simply undefined till set otherwise. TDZ is an ES6 thing.

Upvotes: 1

M Fayaz Q. Rehmani
M Fayaz Q. Rehmani

Reputation: 43

You have to first understand hoisting. Which is taking initializing of code declaration to the top of block, consider following example

function getValue(condition) {
    if (condition) {
        var value = "blue";
        // other code
        return value;
    } else {
        // value exists here with a value of undefined
        return null;
    }
        // value exists here with a value of undefined
}

as you can see the value is accessible in else and also in function. As its being declared straight after getValue(condition) function.

function getValue(condition) {
    if (condition) {
        let value = "blue";
        // other code
        return value;
    } else {
        // value doesn't exist here
        return null;
    }
    // value doesn't exist here
}

But when we use let you can see the difference. The examples are taken from a book I am reading and recommend you to also see

https://leanpub.com/understandinges6/read#leanpub-auto-var-declarations-and-hoisting

for further clarification

Upvotes: 0

Rahul Tripathi
Rahul Tripathi

Reputation: 172628

The documentation says:

The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer's AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

Also the var keyword :

let allows you to declare variables that are limited in scope to the block, statement, or expression on which it is used. This is unlike the var keyword, which defines a variable globally, or locally to an entire function regardless of block scope.

You can also check this article by Kyle Simpson: For and against let

Upvotes: 4

Related Questions