When Can I Use a Variable?
At what point does a variable become available to use within its scope? There may seem to be an obvious answer: after the variable has been declared/created. Right? Not quite. Consider:greeting from line 1 (to retrieve and execute a function reference), even though the greeting() function declaration doesn’t occur until line 4?
Hoisting
The term most commonly used for a variable being visible from the beginning of its enclosing scope, even though its declaration may appear further down in the scope, is called hoisting.Every identifier is created at the beginning of the scope it belongs to, every time that scope is entered.
greeting from the beginning of the scope, but why can we call the greeting() function before it’s been declared?
The answer is a special characteristic of formal function declarations, called function hoisting. When a function declaration’s name identifier is registered at the top of its scope, it’s additionally auto-initialized to that function’s reference.
One key detail is that both function hoisting and var-flavored variable hoisting attach their name identifiers to the nearest enclosing function scope (or, if none, the global scope), not a block scope.
Declarations with
let and const still hoist. But these two declaration forms attach to their enclosing block rather than just an enclosing function as with var and function declarations.Hoisting: Declaration vs. Expression
Function hoisting only applies to formalfunction declarations, not to function expression assignments. Consider:
TypeError means we’re trying to do something with a value that is not allowed.
Notice that the error is not a ReferenceError. JS isn’t telling us that it couldn’t find greeting as an identifier in the scope. It’s telling us that greeting was found but doesn’t hold a function reference at that moment.
What does greeting hold? In addition to being hoisted, variables declared with var are also automatically initialized to undefined at the beginning of their scope. So on that first line, greeting exists, but it holds only the default undefined value.
A
function declaration is hoisted and initialized to its function value (called function hoisting). A var variable is also hoisted, and then auto-initialized to undefined. Any subsequent function expression assignments to that variable don’t happen until that assignment is processed during runtime execution.Variable Hoisting
Let’s look at another example of variable hoisting:greeting isn’t declared until line 5, it’s available to be assigned to as early as line 1. Why?
There’s two necessary parts to the explanation:
- The identifier is hoisted
- And it’s automatically initialized to the value
undefinedfrom the top of the scope
Hoisting: Yet Another Metaphor
Rather than hoisting being a concrete execution step the JS engine performs, it’s more useful to think of hoisting as a visualization of various actions JS takes in setting up the program before execution. The typical assertion of what hoisting means: lifting—like lifting a heavy weight upward—any identifiers all the way to the top of a scope. The explanation often asserted is that the JS engine will actually rewrite that program before execution. I assert that hoisting should be used to refer to the compile-time operation of generating runtime instructions for the automatic registration of a variable at the beginning of its scope, each time that scope is entered.Re-declaration?
What do you think happens when a variable is declared more than once in the same scope? Consider:var studentName has re-declared the variable (and thus “reset” it), so they expect undefined to be printed.
But is there such a thing as a variable being “re-declared” in the same scope? No.
Since hoisting is actually about registering a variable at the beginning of a scope, there’s nothing to be done in the middle of the scope where the original program actually had the second var studentName statement. It’s just a no-op(eration), a pointless statement.
It’s also important to point out that var studentName; doesn’t mean var studentName = undefined;. Let’s prove they’re different:
let or const?
SyntaxError. The error message will indicate something like: “studentName has already been declared.” In other words, this is a case where attempted “re-declaration” is explicitly not allowed!
This is really more of a “social engineering” issue. “Re-declaration” of variables is seen by some, including many on the TC39 body, as a bad habit that can lead to program bugs. So when ES6 introduced
let, they decided to prevent “re-declaration” with an error.Constants?
Theconst keyword is more constrained than let. Like let, const cannot be repeated with the same identifier in the same scope. But there’s actually an overriding technical reason why that sort of “re-declaration” is disallowed.
The const keyword requires a variable to be initialized:
const declarations create variables that cannot be re-assigned:
Loops
So it’s clear from our previous discussion that JS doesn’t really want us to “re-declare” our variables within the same scope. That probably seems straightforward, until you consider what it means for repeated execution of declaration statements in loops:value being “re-declared” repeatedly in this program? Will we get errors thrown? No.
All the rules of scope (including “re-declaration” of let-created variables) are applied per scope instance. In other words, each time a scope is entered during execution, everything resets.
Each loop iteration is its own new scope instance, and within each scope instance,
value is only being declared once. So there’s no attempted “re-declaration,” and thus no error.for-loops?
value declared per scope instance. But what about i?
To answer that, consider what scope i is in. It might seem like it would be in the outer scope, but it’s not. It’s in the scope of the for-loop body, just like value is.
The i and value variables are both declared exactly once per scope instance. No “re-declaration.”
Uninitialized Variables (aka, TDZ)
Withvar declarations, the variable is “hoisted” to the top of its scope. But it’s also automatically initialized to the undefined value, so that the variable can be used throughout the entire scope.
However, let and const declarations are not quite the same in this respect.
Consider:
ReferenceError is thrown on the first line. The error message may say something like: “Cannot access studentName before initialization.”
The error message used to be much more vague or misleading. Thankfully, several in the community were successfully able to lobby for JS engines to improve this error message!
studentName exists on line 1, but it’s not been initialized, so it cannot be used yet.
For let/const, the only way to initialize an uninitialized variable is with an assignment attached to a declaration statement:
var also technically has a TDZ, but it’s zero in length and thus unobservable to our programs! Only let and const have an observable TDZ.
TDZ Errors
Consider:console.log(..) referencing studentName comes after the let studentName declaration, timing wise the askQuestion() function is invoked before the let statement is encountered, while studentName is still in its TDZ! Hence the error.
There’s a common misconception that TDZ means let and const do not hoist. This is inaccurate. They definitely hoist.
Let’s prove that let and const do hoist (auto-register at the top of the scope), courtesy of our friend shadowing:
console.log(..) statement? If let studentName didn’t hoist to the top of the scope, then the first console.log(..) should print "Kyle", right?
But instead, the first console.log(..) throws a TDZ error, because in fact, the inner scope’s studentName was hoisted (auto-registered at the top of the scope). What didn’t happen (yet!) was the auto-initialization of that inner studentName; it’s still uninitialized at that moment, hence the TDZ violation!
Finally Initialized
Working with variables has much more nuance than it seems at first glance. Hoisting, (re)declaration, and the TDZ are common sources of confusion for developers, especially those who have worked in other languages before coming to JS. Hoisting is generally cited as an explicit mechanism of the JS engine, but it’s really more a metaphor to describe the various ways JS handles variable declarations during compilation. Even as a metaphor, hoisting offers useful structure for thinking about the life-cycle of a variable. Declaration and re-declaration of variables tend to cause confusion when thought of as runtime operations. But if you shift to compile-time thinking for these operations, the quirks diminish. The TDZ (temporal dead zone) error is strange and frustrating when encountered. Fortunately, TDZ is relatively straightforward to avoid if you’re always careful to placelet/const declarations at the top of any scope.
