![]() |
In the vast JavaScript ecosystem, few concepts cause as much confusion for beginners—and sometimes even for experienced developers—as hoisting. To master this language, it is essential to move beyond superficial intuition and understand how the JavaScript engine (like V8 in Chrome or SpiderMonkey in Firefox) processes our code before executing it.
1. Theoretical Definition
Hoisting is the default JavaScript behavior where variable and function declarations are symbolically moved to the top of their current scope during the compilation phase, before the code is executed.
It is crucial to clarify a common misconception: the code is not physically moved. The JavaScript engine scans the code, allocates memory for variables and functions, and only then executes the instructions line by line.
2. The Mechanism: Two Execution Phases
To understand hoisting, we must distinguish between the two phases of the execution context:
- Creation Phase (Compilation): JavaScript analyzes the code, finds declarations, and registers them in the Scope Object.
- Execution Phase: The code is executed line by line, assigning values to variables.
3. Analysis by Type
A. Function Declarations
Function declarations are entirely "hoisted." This means the entire function is available to be invoked even before the line where it was written.
greet(); // Output: "Hello!"
function greet() {
console.log("Hello!");
}
B. Variables with var
Variables declared with var are "hoisted" and automatically initialized with the value undefined.
console.log(name); // Output: undefined
var name = "Mario";
console.log(name); // Output: "Mario"
Engine interpretation:
var name; // Declaration hoisted
console.log(name);
name = "Mario"; // Assignment in its original place
C. Variables with let and const (Temporal Dead Zone)
Contrary to popular belief, let and const also undergo hoisting, but they are not initialized. They remain in a region called the Temporal Dead Zone (TDZ) from the creation of the scope until the line where the declaration occurs.
// console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 25;
4. Comparative Example: Functions vs Expressions
A critical error often occurs when confusing a function declaration with a function expression:
// Function Declaration
sum(2, 3); // Works!
function sum(a, b) {
return a + b;
}
// Function Expression
// multiply(2, 3); // Error: multiply is not a function
var multiply = function(a, b) {
return a * b;
};
In the second case, multiply is treated as a var variable (initialized to undefined), so it cannot be invoked as a function before its assignment.
5. More Complex Scenarios
5.1 Arrow Functions and Hoisting
Arrow Functions (const greet = () => {}) are subject to the same rules as const or let (or var if used improperly). They are never hoisted like function declarations.
The behavior
If you try to invoke an arrow function before its definition, the JavaScript engine will throw an error because the variable containing the function has not yet been initialized (in the case of let/const) or is undefined (in the case of var).
// Test: Arrow function with const
// greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => console.log("Hello!");
// Test: Arrow function with var
// greetVar(); // TypeError: greetVar is not a function
var greetVar = () => console.log("Hello!");
This happens because, unlike Function Declarations, the arrow function is an expression assigned to a variable. The engine hoists the container (the variable), but not the assignment of the function itself.
5.2. Block Scoping and the TDZ (Temporal Dead Zone)
The concept of block scoping (introduced by ES6) has drastically transformed how we handle hoisting. With let and const, hoisting does not disappear, but it becomes "silent" and protected by the Temporal Dead Zone.
The TDZ is the time interval between the start of the scope (the { ... } block) and the line where the variable is declared. During this interval, the variable exists in the context, but is inaccessible.
Complex example in an if block
let name = "Outside";
if (true) {
// Start of the block (start of the TDZ for 'name')
// console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "Inside"; // End of the TDZ
console.log(name); // "Inside"
}
Why is this crucial?
- Bug prevention: Prevents using variables before they have been properly initialized.
- Shadowing: Allows "hiding" an external variable with an internal one within the same block, without creating unexpected logical conflicts (as happened with
var, which ignores blocks and hoists everything to the function level).
5.3. Complexity: Hoisting in for loops
A common use case that benefits from block scoping is the for loop.
- With
var: The variableiis hoisted only once for the entire function. If we use a closure (e.g., asetTimeoutinside), all calls will point to the same final value ofi. - With
let: The engine creates a new "instance" of the variableifor every iteration of the block.
// Example with var (often a source of bugs)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Prints 3, 3, 3
}
// Example with let (expected behavior)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100); // Prints 0, 1, 2
}
In summary, the {} "block" acts as a barrier. With let and const, each iteration of the for loop is considered a unique block, isolating the variable's value and making hoisting a protective mechanism rather than a trap.
6. Summary and Best Practices
To write clean and predictable (maintainable code), follow these guidelines based on software engineering best practices:
- Avoid
var: Always useletandconst. This eliminates surprises related to initialization toundefined. - Declare at the top: Even if the engine handles hoisting, code readability improves significantly if you declare variables and functions at the beginning of their scope.
- Be explicit: Do not rely on hoisting to invoke functions before their definition; it is a practice that makes the program's logical flow difficult to follow.
Hoisting is not a bug, but an intrinsic feature of how JavaScript handles scope. Understanding this distinction allows you to turn a potential source of errors into solid knowledge of how the interpreter works internally.
Follow me #techelopment
Official site: www.techelopment.it
facebook: Techelopment
instagram: @techelopment
X: techelopment
Bluesky: @techelopment
telegram: @techelopment_channel
whatsapp: Techelopment
youtube: @techelopment
