What is Hoisting?
Hoisting refers to the specific behavior of the JS interpreter, where variables and function declarations are moved to the top of their containing scope during the compile phase, before the code has been executed.
It's important to note that only the declarations are hoisted not the initializations. This means if a variable is declared and initialized after using it, the variable will be undefined
, because only the declaration was hoisted to the top - not the initialization.
Variable Hoisting
Let's explore how hoisting works in two scenarios: the traditional way using var and the modern way using ES6 declarations (let
and const
).
Traditional Hoisting with var
In the good ol’ days, the primary way to declare variables was using the var
keyword.
When a variable is declared using var
, it gets hoisted to the top of its function or global scope (note that while the declarations are hoisted, the initializations are not. This means if you try to use a variable before it is initialized, you will get a result of undefined
.
For example:
console.log(message); // undefined
var message = "Hello, hoisting!";
console.log(message); // Hello, hoisting!
In the example above, even though I’m trying to log the message variable before its declaration, JavaScript hoists the declaration to the top, making it undefined
at the first log statement and then assigning the value later.
⚠️ This behavior can lead to unexpected results and bugs, especially if you're not aware of how hoisting works.
Modern Hoisting with ES6 let
and const
The let
and const
also have a hoisting behavior, but it's slightly different from var
.
When a variable is declared using let
, it gets hoisted to the top of the block, but it's not initialized. This means that you cannot access the value of a variable until it's reached in the code (you’ll get Reference Error
).
The const
variable means that it contains a constant value and it can’t be updated / re-declared.
const
declarations are hoisted too but they are not initialized (just likelet
).
Unlike var, the let and const
declarations have block-level scope.
let cherryStatus = "ripe";
let quantity = 5;
if (quantity > 3) {
let message = "pick cherries!";
console.log(message); // "pick cherries!"
}
console.log(message); // ReferenceError: message is not defined
Here, I am trying to access message outside the block where it was declared. But as a result I’m getting an error due to the block-scoping nature of the let
keyword, which prevents the variable from being accessible outside the block in which it’s defined.
Another example with let
:
console.log(message); // ReferenceError: Cannot access 'message' before initialization
let message = "Hello, ES6 hoisting!";
console.log(message); // Hello, ES6 hoisting!
In this case, trying to access the message variable before its declaration results in a ReferenceError
. The variable is hoisted to the top of its block scope, but it is not initialized until the declaration statement.
Similarly, using const
will also result in a ReferenceError
if accessed before declaration:
console.log(pi); // ReferenceError: Cannot access 'pi' before initialization
const pi = 3.14159;
console.log(pi); // 3.14159
💡 When attempting to access a
let
orconst
variable before its declaration, a reference error occurs due to the presence of the temporal dead zone (TDZ)
The temporal dead zone
The Temporal Dead Zone (TDZ) in JavaScript refers to the period between the entering of scope and the point where a variable is declared. During this temporal dead zone, if you try to access the variable before its declaration, you will get a ReferenceError
. This concept is particularly associated with the let
and const
declarations in ECMAScript 6 (ES6) and later..
Here’s an example:
function example() {
// TDZ for 'a' starts here
console.log(a); // ReferenceError: Cannot access 'a' before initialization
// TDZ for 'a' ends here
let a = 42;
console.log(a); // 42
if (true) {
// TDZ for 'b' starts here
console.log(b); // ReferenceError: Cannot access 'b' before initialization
// TDZ for 'b' ends here
let b = 7;
console.log(b); // 7
}
// TDZ for 'c' starts here
console.log(c); // ReferenceError: Cannot access 'c' before initialization
// TDZ for 'c' ends here
const c = 99;
console.log(c); // 99
}
example();
Key Differences and Best Practices
- Scope: var has function-level,
let
andconst
have block-level scope, reducing unintended side effects. - Hoisting: var hoists the entire declaration, often causing
undefined
initial value.let
andconst
hoist only declaration, accessing them before results in ReferenceError. - Reassignment:
const
can't be reassigned, enhancing predictability and maintainability.
Modern declarations (let
and const
) mitigate traditional var hoisting issues and align with best practices.
Function Hoisting
Function declarations are also hoisted in JavaScript, which means you can use a function before it's declared in your code.
This is different from variable hoisting, because for functions, both the declaration and the function body are hoisted. Here's an example:
console.log(myFunction()); // output: "Hello, World!"
function myFunction() {
return "Hello, World!";
}
In this case - myFunction()
is called before it's declared, but the code still works because of function hoisting.
Just like with variables, the function declaration - myFunction()
is hoisted to the top, allowing us to call the function before its actual declaration.
Behind the scenes, the code is interpreted like this:
function sayHello() {
console.log("Hello, hoisting!");
}
sayHello(); // Output: Hello, hoisting!
Conclusion
In conclusion, JavaScript hoisting is a unique feature that can cause some unexpected results if not fully understood. By being aware of how JavaScript hoists variables and functions, you can write cleaner and more predictable code. The key takeaways are that var
and function declarations are hoisted to the top of their scope and let
and const
are hoisted to the top of their block but are not initialized until their declaration is encountered in the code.