Reputation: 50346
I understand that let
will be hoisted to top of the block, but accessing it before initializing will throw ReferenceError
due 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
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:
var
is in functions); orlet
, const
, class
, etc.; orOption 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 throwundefined
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:
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:
example
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
.console.log("alpha")
.a = 1
, changing the value of the binding for a
from undefined
to 1
.let b
, changing the b
binding's "accessible" flag to true.b = 2
, changing the value of the binding for b
from undefined
to 2
.Math.random() < 0.5
; let's say it's true:c
and e
to that LEO's envrec, with their "accessible" flags set to false.console.log("beta")
.let c = 3
, setting c
's binding's "accessible" flag to true and setting its value to 3
d = 4
.console.log("gamma")
.let e = 5
, setting e
's binding's "accessible" flag to true and setting its value to 5
.console.log(a, b, c, d, e)
.Hopefully that answers:
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)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
Reputation: 10459
A Saying a let
variable is not hoisted.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
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
?
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.
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
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
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