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:
- Have block scope but with all the declarations hoisted to the top and accessible (like varis in functions); or
- Don't have block scope, and instead have a new scope start with every let,const,class, etc.; or
- 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:
- It creates an LEO for the context of the call to example
- It adds bindings for a,b, anddto that LEO's envrec, all with the valueundefined:
- ais added because it's a- varbinding located anywhere in the function. Its "accessible" flag is set to true (because of- var).
- bis added because it's a- letbinding at the top level of the function; its "accessible" flag is set to false because we haven't reached the- let bline yet.
- dbecause it's a- varbinding, like- a.
- It executes console.log("alpha").
- It executes a = 1, changing the value of the binding forafromundefinedto1.
- It executes let b, changing thebbinding's "accessible" flag to true.
- It executes b = 2, changing the value of the binding forbfromundefinedto2.
- It evaluates Math.random() < 0.5; let's say it's true:
- 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.
- It adds bindings for candeto that LEO's envrec, with their "accessible" flags set to false.
- It executes console.log("beta").
- It executes let c = 3, settingc's binding's "accessible" flag to true and setting its value to3
- It executes d = 4.
- It executes console.log("gamma").
- It executes let e = 5, settinge's binding's "accessible" flag to true and setting its value to5.
- It executes console.log(a, b, c, d, e).
Hopefully that answers:
- Why we have lethalf-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 onesvarhoisting had at the function level)
- Why vardoesn't have a TDZ (avarvariable'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.