Closed dxu closed 8 years ago
Hello,
Hoisting is the topic of the next chapter in that book:
https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch4.md
the scope application of hoisting starts in this part specifically:
EDIT: phrasing
Thanks for the comment. If you take a look at the chapter you link to, as well as my example, you'll notice that the chapter on hoisting actually doesn't address the specifics of hoisting with the let
and const
bindings.
Hoisting is shown in the chapter as a step of the compiler that lifts var
declarations to the top of the scope, as shown in this example:
console.log( a );
var a = 2;
into something that is "invisibly" interpreted during compilation as something more similar to
var a;
console.log( a );
a = 2;
According to this interpretation, it'd be easy for someone to make the assumption regarding the clause I quoted above:
However, declarations made with let will not hoist to the entire scope of the block they appear in. Such declarations will not observably "exist" in the block until the declaration statement.
{ console.log( bar ); // ReferenceError! let bar = 2; }
For someone unfamiliar with the nuances around let
, but familiar with the classical "hoisting" mechanics, they might inaccurately assume this means that in the following example:
let x = 'outer scope';
(function() {
console.log(x); // Reference error
let x = 'inner scope';
}());
console.log(x)
will print the x of the outer scope ("outer scope"
), because the phrasing:
declarations made with let will not hoist to the entire scope of the block they appear in
is misleading. x
is still technically "hoisted", in the sense that something is being created that is identified by x
at the top of the scope, it's just not initialized. As a result, console.log(x)
will actually throw a Reference Error.
Let me know my explanation makes sense. You can also take a look at the article linked above - he explains in further detail about how the Temporal Dead Zone works.
EDIT: phrasing on last paragraph to more clearly outline what happens in the example.
@dxu Maybe the error message is clear enough. Reference error
means the engine can not resolve the name x
----> not hoisted.
Hoisting is not a real thing. It's a made up concept. It's a metaphor to describe what actually happens when the compiler finds declarations and associates them with the scope they belong to so they can be used in that scope.
When we say that this code:
x = 2;
var x;
is interpreted as this code via hoisting:
var x;
x = 2;
...what we're actually saying is not just that the x
is added to the scope by the compiler, but that x
is "initialized" so that it can be used in the entire scope. "Initialized" here has nothing to do with the x = 2
part, though it'd be tempting to assume such as obvious; it actually is an internal concept of the spec.
With var
declarations, there is no way to distinguish between a declared variable and an initialized variable, because var
declarations are automatically initialized at the beginning of the scope, so they are available for use throughout the scope. You cannot observe an "uninitialized" var
declaration.
let
declarations however are not initialized at the beginning of the scope; their initialization doesn't come in the block until the actual line where let x
occurs (again, regardless of any = ..
value assignment).
So to recap: the metaphor of hoisting is about both making a declaration attach to a scope at compile time before execution, and about initializing that variable to be available throughout the scope. Both are true of var
declarations, but only the first one is true of let
and const
declarations. Therefore, let
and const
are in fact not hoisted.
To the confusion that you're pointing out around not being able to access the outer x
while on the first line of the inner scope, you're absolutely correct that this can be confusing. It's just it's inaccurate to explain that situation as "not hoisting".
Whenever I teach, I only mention such a thing in passing as an informal explanation, which is kinda fudging with the truth a bit. Then again, the entire usage of "hoisting" to describe JS's compiler scoping behavior is fudging with the truth, because hoisting implies a reordering of code, which is not at all what happens.
The accurate explanation of the confusion you point out is that var
has no TDZ (temporal dead zone) because the initialization comes immediately at the beginning of the scope, whereas let
and const
have a TDZ since the initialization is deferred until "later" when the actual declarator is encountered.
A variable cannot be used while it's in its TDZ, which is why you're able to observe the inaccessibility of an very-much-already-existent-but-not-yet-initialized inner x
in your example. The ReferenceError
in that sense is not entirely accurate: it's not that x
hasn't been declared, it's that it hasn't been initialized yet.
I cover TDZ in more detail in subsequent books in the series ("Types & Grammar" and "ES6 & Beyond"). At the time of the writing of this book, I didn't have as full an understanding of TDZ and its implications on how scopes work as I do now, which is why it's not mentioned here. It's a planned issue to address TDZ in the second edition of the book, whenever I get around to that effort.
@getify let
and const
are hoisted as far as i know. Here is an example:
function foo(value) {
var x = 1
if (true) {
/*
* TDZ for x
*/
console.log(x) // ReferenceError: x is not defined
let x = 2 // here TDZ ends and x get its value
console.log(x) // => 2
}
}
foo()
This demostrates that let x
is hoisted to the top of the if
block, because if it weren't hoisted, we will not get any ReferenceError
and we would get 1
instead (from var x
). But the hoisting concept creates an undefined reference at the top of the if
block.
We can't use it and get undefined
like we would do with var
because TDZ doesn't let us access that reference until assignment, no matter if it is or not initialized because of hoisting.
I'm missing something?
@felixsanz Hoisting is used to describe the creation of the binding and the initialization. I think even members of the committee do not consider let
and const
to be hoisted. Hoisting really implies the initialization part. There probably isn't a dedicated term for the "binding creation before code execution" part.
I also only considered the mere fact that the bindings are created as hoisting, but that's not technically true. Otherwise, why wouldn't let
and const
be _HoistableDeclaration_s?
@felixsanz
let and const are hoisted as far as i know.
Nope.
This demostrates that let x is hoisted to the top of the if block, because if it weren't hoisted, we will not get any ReferenceError
Nope. Please re-read my message above where I correct this misunderstanding.
Quoting myself:
hoisting ... actually saying is not just that the x is added to the scope by the compiler, but that x is "initialized" so that it can be used in the entire scope.
What @fkling said is correct.
@getify Why you say that hoisting means adding the variable to the scope and ALSO initializing it so it can be used in the entire scope? I mean, where do you get that definition from?
Hoisting is the act of hoist, which means in the dictionary "raising" or "lifting". And this is true since console.log(x)
in my code produces a ReferenceError
. So it's raised/lifted. It's not initialized i agree with that, but it's hoisted :/
Some source for what hoisting really means in programming would be nice, because if it's a concept/idea, then everyone can have its own definition (and that doesn't mean you are wrong, but means that we both could be right).
Also:
http://www.ecma-international.org/ecma-262/7.0/#sec-evaldeclarationinstantiation
5 -> d -> ii -> 1
NOTE: The environment of with statements cannot contain any lexical declaration so it doesn't need to be checked for var/let hoisting conflicts.
"let hoisting".
I think we're arguing about semantics and wording here. I think everyone's agreed on how this actually works - it's just the interpretation of what "hoisting" is strictly referring to. There's definitely a difference between the var
hoisting of the past and the new TDZ behavior with let
, so there is a case to be argued that the act of "hoisting" should refer to both preinitialization and initialization. Regardless, I don't think it's worth arguing about, at least not in this context.
Thanks for the great work @getify! I look forward to finishing up the rest of the book. Thanks for all your contributions to the community, I really enjoyed your talk at Forward 3 last year.
I mean, where do you get that definition from?
https://twitter.com/awbjs/status/434044880180871168
(@allenwb is the editor of the ES6 spec, so I trust his authority on the terms/definitions.)
Thanks for @getify 's nice explanation. I get the key points:
declare
and initialize
.var
do both declare
and initialize
, the two cannot be split for var
.let
do firstly declare
in the top of the scope, and do initialize
when encounter the let xxx
statements.The fourth point is important that
let x
, outer x
will be covered.let x
, x
is then initialized and can be used. Before all usage will lead to error because of TDZ.But I'm still confused about the meaning of declare
and initialize
.
I thought the declared variable (has value undefined
) can be used and initialized variable means the variable has value.
But I'm still confused about the meaning of declare and initialize.
I thought the declared variable (has value undefined) can be used and initialized variable means the variable has value.
Yeah, the problem is nuances and conflations of words.
From the code author's perspective, "declaring" is the var x
part and "initializing" is the x = 2
part. But from the perspective of the spec/engine, these shift. "Declaring" is like registering a variable to a scope, "initializing" is reserving space/memory/binding for that variable so it can be used (and giving it its initial undefined
value), and "assigning" is giving it a value explicitly in code.
Declaring always happens at time of compilation, and its effect can be seen whenever a scope is first entered. Initializing for var
happens at the beginning of the scope, whereas it happens at the site of the declarator for let
and const
. Initialization is what gives a value its initial undefined
value. Assignment then is when you actually use =
to assign something to it.
Here's the real question. What is the benefit of changing the behavior of let and const from the way var works?
Mostly to prevent code that does silly things like:
x = 2;
var x;
By far, most developers would agree this is poor coding style. So, the language applies social pressure to devs to convince them to stop doing that so theu can use the shiny new toys.
From the code author's perspective, "declaring" is the var x part and "initializing" is the x = 2 part. But from the perspective of the spec/engine, these shift. "Declaring" is like registering a variable to a scope, "initializing" is reserving space/memory/binding for that variable so it can be used (and giving it its initial undefined value), and "assigning" is giving it a value explicitly in code.
The spec. uses the phrase "create a binding" to mean "registering a variable in a scope". A binding is an association between a name and a value. Bindings are created for all the names declarated within the scope when the scope is first entered. When a binding is created it does not yet have a value associated with it. It is "uninitialized". Determination of when a binding actually gets initialized depends upon how its name was declared and the kind of scope that contains itl.
Mostly to prevent code that does silly things like:
x =2; var x;
Except, the above is legal. The real motivation for temporal deed zones is:
console.log(k);
x=42;
const k=x;;
console.log(k);
Should the first log
be legal and if so what value does it see for k
? const k
is intended to mean: all accesses to k
will have the same value which is the value of it's initialization expression. If the first log
reported that the value of k
was undefined
and the second log
report that its value is 42
then the value of k
would have observably changed, invalidating the const
invariant. So, the spec. says that a runtime exception occurs if k
is accessed before it is initialized. The period of time between between the creation of the binding for k
and the initialization of k
is its temporal dead zone. T
For consistency, all new declarations introduced by ES6 follow the same initialization rule as const
.
Except, the above is legal.
Of course the above is legal. I intentionally used a var
because that code snippet was the bad legacy thing (relying on variable hoisting) that developers were doing that they shouldn't have been doing. So when they change to x = 2; let x;
, now they'll get a compiler error to tell them not to do it.
@getify Thank you for the nice details about what's hoisted and what's not. To continue & detail the explanations, I created an interesting blog post: https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
:+1
Interesting discussion here. I agree that at this point people are basically arguing about the semantics of what the term hoisting means. MDN states here that let is hoisted which will only add to the confusion. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
@getify I can't agree more. in my opnion, hoisting is only a concept which was created for us to better unstand how var expression was evaluated
console.log(a) // undefined
var a = 1
console.log(a) // 1
equals to
var a
console.log(a)
a = 1
but the compiler really just replace the code? i'm not sure. Since let and const are either the same nor exactly opsite to var due to TDZ. so the concept hoisting may not be suitable for let/const.
Is 'hoisting' a valid term/concept in specification, now? Is this tweet https://twitter.com/awbjs/status/434044880180871168 written at 2014 would be old answer?
Update: The links below are very helpful discussion for me. Refer to them. https://twitter.com/awbjs/status/1133756684340420609 https://twitter.com/awbjs/status/1149390012120612864
Just for the record... I was wrong in this thread years ago. "Hoisting" is attaching the binding, only, not whether it's initialized. Let and const "hoist" but they aren't initialized. My apologies for the confusion caused. I know better now.
@getify So, If the hoisting means what you said, should explanation below be updated?
However, declarations made with let will not hoist to the entire scope of the block they appear in. Such declarations will not observably "exist" in the block until the declaration statement.
Update:
See https://github.com/getify/You-Dont-Know-JS/issues/1132 together.
IOW, let and const declarations do in fact "hoist" their variable declarations to the entire enclosing scope (just like var), but they remain uninitialized (unlike var) and thus cannot be accessed earlier (from an execution perspective, not a code location perspective) in the scope than their declaration point.
For clarity on my position:
For the clarity, sorry, i don't fully understand your meaning. What's your meaning of "scope is entered"? At compile time? By whom?
"scope is entered" is referring not to compile time but to runtime execution... each time a scope is executed. parsing/compilation sets up the plan for a scope, but this is just a plan. the scope itself isn't created (memory reserved, etc) until it's executed.
Thanks!! To sum up, the phases to execute declarations(var
, let
and const
) are divided into declaration phase which is a plan for a scope performed at compile time and initialization phase which is a performed at runtime, assigning a value to variable(constant) at those variables declaration site. The gap between those phases is called as temporal dead zone.
Yes, and additionally, there is no "gap" for var
; they're auto-initialized as soon as the scope is instantiated.
You are right! So temporal dead zone doesn't exist in case of var
.
The spec. uses the phrase "create a binding" to mean "registering a variable in a scope". A binding is an association between a name and a value. Bindings are created for all the names declarated within the scope when the scope is first entered. When a binding is created it does not yet have a value associated with it. It is "uninitialized". Determination of when a binding actually gets initialized depends upon how its name was declared and the kind of scope that contains itl.
I don't believe I have fully understood the "binding" in "create a binding" part, since I had seen it popping up in places like const declaration, in which someone pointed out that it creates an "immutable binding".
"Immutable binding" is, IMO, a bit misleading. I would say: const
creates an immutable assignment.
As commonly used in programming language literature, a "binding" is an association, within an environment, between a name and some entity. In JS, most bindings are to computational "values", statement labels are also a kind of binding.
An immutable binding is one whose entity association never changes. A mutable binding us one whose entity association can change.
Mutability of bindings is an orthogonal concept to mutability of entities (eg values). A JS let or var declaration creates a mutable binding. The associated entity (at some point in time) might be either an immutable value (a number, string, etc) or a mutable value (most objects). A const declaration creates an immutable binding, but just like a let/var the associated entity might be either mutable or immutable.
BTW, extra credit question.
In JS, a binding can usually be thought of as associating a name with a value. Some language have features that require bindings that associate a name with a storage cell. For example, call-by-reference parameters. This enables multiple names to refer to the same "variable". What features in modern JS create bindings to storage cells rather than values.
Shrugs. I suppose I don't really see it useful to use the word "binding" to talk about the "assignment of a value", why not just talk about that as "assignment". To me, "binding" is more about the attachment between a variable name and the scope it's in... its entry in the lexical environment.
But whatever. It's not really worth nitpicking over these words.
What features in modern JS create bindings to storage cells rather than values.
What comes to mind: arguments
, and also the identifier(s) import
ed from an ES6 module.
In general, I think it's fine to talk about "assignment of a value to a variable". Talking about changing a binding is a way to define what is "assignment" means.
Right on import, but also applies to export. Export also binds to storage cells.
For the sake of completeness, here's a Lua sample. Lua local
variables are let
-like (lexical, per block, and mutable), but without hoisting.
The scope starts at the declaration and last until either the end of the block or until it is shadowed by an identically named variable.
local a = 5
local function factory()
local function outer()
return a
end
local a = 6
local function firstInner()
return a
end
local a = 7
local function secondInner()
return a
end
return {outer = outer, firstInner = firstInner, secondInner = secondInner}
end
o = factory()
print(o.outer()) --> 5
print(o.firstInner()) --> 6
print(o.secondInner()) --> 7
IMO let
is better from an ergonomics standpoint. Lua is like that because it has a single pass parser/bytecode compiler, and implementation simplicity is factored in the language design.
Firefox had a let block
form, like:
let (x = 2) {
// ..
}
I think this was a million times better than the let
declaration form we got. Unfortunately, TC39 rejected the let
block (in favor of just { let x = 2; .. }
).
Hi, thanks for taking the time to write such a great book! I'm working my way through it - in Chapter 3, you talk about hoisting
let
variables. Specifically:Even though the variable
bar
isn't initialized as undefined, it seems that they are still created.Using the example from this article as an example,
Do you think it would be helpful to add a clarification regarding the meaning of "hoisting" (since
x
here is technically still being created at the top of the block - it's just not initialized and can't be accessed), or perhaps just include a similar example?EDIT: removed extra console.log in the example that may have caused confusion