codelessgenie guide

Understanding Hoisting in JavaScript: A Complete Guide

If you’ve spent any time writing JavaScript, you’ve likely encountered a scenario where a variable or function seems to “work” even when referenced *before* it’s declared in the code. Or perhaps you’ve been confused by a `ReferenceError` when trying to access a variable that *looks* like it should exist. These behaviors are often tied to a fundamental JavaScript mechanism called **hoisting**. Hoisting is one of the most misunderstood concepts in JavaScript, yet it’s critical to writing predictable, bug-free code. In this guide, we’ll demystify hoisting: what it is, how it works under the hood, how it affects variables, functions, and classes, common pitfalls to avoid, and best practices to leverage it effectively. By the end, you’ll have a clear understanding of why JavaScript behaves the way it does and how to use hoisting to your advantage.

Table of Contents

  1. What is Hoisting?
  2. How Hoisting Works Under the Hood
  3. Variable Hoisting: var, let, and const
  4. Function Hoisting
  5. Hoisting with Classes
  6. Common Pitfalls and Misconceptions
  7. Best Practices to Avoid Hoisting Issues
  8. Conclusion
  9. References

1. What is Hoisting?

At its core, hoisting is a JavaScript engine behavior where declarations of variables, functions, and classes are moved to the “top” of their containing scope during the compilation phase—before the code is executed.

Wait, “moved to the top”? Not literally. The JavaScript engine doesn’t physically rearrange your code. Instead, during the compilation phase (before execution), it scans the code to identify all declarations and sets up memory space for them. This makes declarations “available” earlier in the code than they appear, giving the illusion that they’ve been “hoisted” upward.

Key Note: Hoisting affects declarations, not initializations or assignments. Only the name of the variable/function/class is hoisted; any value assigned to it remains in its original position in the code.

2. How Hoisting Works Under the Hood

To understand hoisting, we need to peek into how the JavaScript engine processes code. JavaScript execution happens in two main phases:

Phase 1: Creation Phase

Before any code runs, the engine parses the code and sets up the execution context (the environment in which code runs). During this phase:

  • The engine identifies all variable, function, and class declarations in the current scope.
  • It allocates memory for these declarations, “hoisting” them to the top of their scope.
  • Variables declared with var are initialized with undefined.
  • Variables declared with let/const and classes are hoisted but not initialized (they enter a “temporal dead zone,” explained later).
  • Function declarations are fully hoisted (both name and body are stored in memory).

Phase 2: Execution Phase

The engine runs the code line by line. During this phase:

  • Variables are assigned their values (if any).
  • Functions and classes are executed or instantiated.

This two-phase process explains why declarations are accessible before their physical placement in the code. Let’s dive deeper into how hoisting affects specific language features.

3. Variable Hoisting: var, let, and const

Variable hoisting behaves differently depending on whether you use var, let, or const. Let’s break down each case.

Hoisting with var

Variables declared with var are function-scoped (or global-scoped if declared outside a function) and are hoisted to the top of their scope. During the creation phase, they are initialized with undefined.

Example:

console.log(x); // Output: undefined (x is hoisted and initialized to undefined)
var x = 10; 
console.log(x); // Output: 10 (x is assigned 10 during execution)

Here’s how the engine processes this code:

  • Creation Phase: var x is hoisted to the top of the scope and initialized to undefined.
  • Execution Phase:
    • console.log(x) runs, printing undefined.
    • x = 10 assigns the value 10 to x.
    • console.log(x) prints 10.

Hoisting with let and const

ES6 introduced let and const, which are block-scoped (confined to { } blocks like loops or conditionals). Unlike var, let and const are hoisted but not initialized during the creation phase. Instead, they enter a temporal dead zone (TDZ)—a period between the start of the scope and the variable’s declaration where accessing the variable throws an error.

Example with let:

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20; 

Example with const:

console.log(z); // ReferenceError: Cannot access 'z' before initialization
const z = 30; 

Why the error? During the creation phase, y and z are hoisted to the top of their block scope but not initialized (unlike var). Accessing them before their declaration line (where they are initialized) triggers a ReferenceError.

The Temporal Dead Zone (TDZ)

The TDZ is the interval from the start of the scope until the variable is declared. For let/const, any attempt to read/write the variable in this zone fails.

Example of TDZ in a Block:

{ // Start of block scope
  console.log(a); // ReferenceError (TDZ active)
  let a = 5; // a is initialized here (end of TDZ)
  console.log(a); // Output: 5 (TDZ ended)
}

The TDZ exists to catch bugs: if you accidentally reference a variable before declaring it, JavaScript throws an error instead of silently returning undefined (as with var).

4. Function Hoisting

Functions in JavaScript are also hoisted, but their behavior depends on whether they are declared as function declarations or function expressions.

Function Declarations

A function declaration has the syntax:

function myFunction() { /* ... */ }

Function declarations are fully hoisted: both the function name and its body are hoisted to the top of the scope. This means you can call the function before it’s declared in the code.

Example:

greet(); // Output: "Hello, hoisting!" (function is hoisted)

function greet() {
  console.log("Hello, hoisting!");
}

During the creation phase, the entire greet function is stored in memory, so calling it before the declaration works.

Function Expressions

A function expression assigns a function to a variable, e.g.:

const myFunction = function() { /* ... */ }; // Anonymous function expression
const myFunction = () => { /* ... */ }; // Arrow function expression

Function expressions are not hoisted as functions. Only the variable itself is hoisted (following the rules of var, let, or const), but the function body is not.

Example with var:

greet(); // TypeError: greet is not a function (var greet is hoisted as undefined)
var greet = function() {
  console.log("Hi!");
};

Here:

  • var greet is hoisted and initialized to undefined (like var variables).
  • Calling greet() before the assignment tries to invoke undefined, throwing a TypeError.

Example with let/const:

greet(); // ReferenceError (let greet is in TDZ)
let greet = function() {
  console.log("Hi!");
};

Since let is hoisted but not initialized, accessing greet before declaration triggers a ReferenceError (TDZ).

5. Hoisting with Classes

Classes in JavaScript (introduced in ES6) also exhibit hoisting behavior, but with nuances similar to let/const.

Class Declarations

Class declarations are hoisted but enter the temporal dead zone (like let/const). They are not initialized during the creation phase, so you cannot instantiate a class before its declaration.

Example:

const car = new Car(); // ReferenceError: Cannot access 'Car' before initialization
class Car {
  constructor(make) {
    this.make = make;
  }
}

The Car class is hoisted to the top of the scope but remains in the TDZ until its declaration line. Accessing it before that throws a ReferenceError.

Class Expressions

Class expressions (like const MyClass = class { ... }) behave similarly to function expressions. The variable is hoisted (following let/const rules), but the class itself is not initialized until the declaration line.

Example:

const dog = new Dog(); // ReferenceError (Dog is in TDZ)
const Dog = class {
  constructor(name) {
    this.name = name;
  }
};

6. Common Pitfalls and Misconceptions

Hoisting can lead to subtle bugs if misunderstood. Here are key pitfalls to watch for:

1. Overwriting Variables with var

Since var is function-scoped and hoisted, redeclaring variables in the same scope can overwrite existing values unexpectedly.

Example:

var count = 10;
function logCount() {
  console.log(count); // undefined (var count is hoisted here, shadowing the global count)
  var count = 20;
  console.log(count); // 20
}
logCount();

The local var count is hoisted to the top of logCount, shadowing the global count and initializing to undefined.

2. Confusing Function Declarations and Expressions

Accidentally using a function expression when you meant to use a declaration can lead to errors.

Example:

// Intended to use a declaration, but wrote an expression (var)
greet(); // TypeError: greet is not a function
var greet = function() {
  console.log("Oops!");
};

3. Ignoring the Temporal Dead Zone

Assuming let/const variables are not hoisted (they are!) can lead to ReferenceErrors.

Example:

{
  //误以为 let 变量不会提升,实际处于 TDZ
  console.log(age); // ReferenceError
  let age = 30;
}

4. Hoisting in Loops with var

var’s function-scoping can cause unexpected behavior in loops, as the same variable is reused across iterations.

Example:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // Logs: 3, 3, 3 (not 0, 1, 2)
}

Here, var i is hoisted to the function scope, so all setTimeout callbacks reference the same i (which becomes 3 after the loop ends). Using let fixes this (block-scoped i per iteration).

7. Best Practices to Avoid Hoisting Issues

To write clean, predictable code, follow these best practices:

1. Use let/const Instead of var

let and const are block-scoped and have TDZ protection, which helps catch errors early. const is preferred for variables that won’t be reassigned (most cases).

2. Declare Variables/Classes Before Use

Even though function declarations are hoisted, declaring them before use improves readability. For variables and classes, always declare them at the top of their scope (or just before they’re needed) to avoid TDZ errors.

3. Prefer Function Declarations for Hoistable Logic

If you need a function to be callable anywhere in its scope, use a function declaration. For one-off or conditional logic, use expressions.

4. Avoid Redeclaring Variables

Redeclaring variables with var (e.g., var x = 5; var x = 10;) is allowed but confusing. let/const prevent redeclaration in the same scope, throwing a SyntaxError.

5. Use Strict Mode

Strict mode ('use strict';) doesn’t change hoisting behavior, but it enables stricter error checking (e.g., preventing accidental global variables), which helps catch hoisting-related bugs.

8. Conclusion

Hoisting is a foundational JavaScript mechanism that affects how variables, functions, and classes are processed before execution. By understanding:

  • The two-phase execution model (creation vs. execution),
  • How var, let, and const differ in hoisting and initialization,
  • The behavior of function declarations vs. expressions,
  • The temporal dead zone for let, const, and classes,

you can write code that avoids common pitfalls and behaves predictably. Remember: hoisting isn’t magic—it’s just the JavaScript engine preparing your code for execution. With this knowledge, you’ll debug faster and build more robust applications.

9. References


I hope this guide clarifies hoisting for you! Let me know in the comments if you have questions or examples to share. Happy coding! 🚀