Docs

12.3-Hoisting-Deep-Dive

6.3 Hoisting Deep Dive

Introduction

Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the creation phase. While it might seem magical, it's actually a predictable result of how execution contexts are created.


What Gets Hoisted?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 HOISTING SUMMARY                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                         β”‚
β”‚  βœ… Function declarations    - Fully hoisted            β”‚
β”‚  βœ… var declarations         - Hoisted (as undefined)   β”‚
β”‚  ⚠️  let declarations        - Hoisted (but in TDZ)     β”‚
β”‚  ⚠️  const declarations      - Hoisted (but in TDZ)     β”‚
β”‚  βœ… Class declarations       - Hoisted (but in TDZ)     β”‚
β”‚  ❌ Function expressions     - NOT hoisted              β”‚
β”‚  ❌ Arrow functions          - NOT hoisted              β”‚
β”‚                                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Function Declaration Hoisting

Function declarations are fully hoistedβ€”both the name and the implementation:

// This works!
sayHello(); // "Hello!"

function sayHello() {
  console.log('Hello!');
}

What JavaScript Sees:

// Creation phase
function sayHello() {
  console.log('Hello!');
}

// Execution phase
sayHello(); // "Hello!"

Multiple Functions:

// Both are available from the start
first(); // "First!"
second(); // "Second!"

function first() {
  console.log('First!');
}

function second() {
  console.log('Second!');
}

var Hoisting

Variables declared with var are hoisted, but only the declaration, not the initialization:

console.log(message); // undefined (not an error!)
var message = 'Hello';
console.log(message); // "Hello"

What JavaScript Sees:

// Creation phase
var message; // Declaration hoisted, initialized to undefined

// Execution phase
console.log(message); // undefined
message = 'Hello'; // Assignment happens here
console.log(message); // "Hello"

Common Mistake:

function example() {
  console.log(x); // undefined (not an error!)

  if (true) {
    var x = 10; // Hoisted to function scope!
  }

  console.log(x); // 10
}
example();

let and const Hoisting (Temporal Dead Zone)

let and const ARE hoisted, but they're in a "Temporal Dead Zone" (TDZ) until the declaration is reached:

// TDZ starts here for 'name'
console.log(name); // ReferenceError: Cannot access 'name' before initialization

let name = 'Alice'; // TDZ ends here

console.log(name); // "Alice"

Temporal Dead Zone Visualized:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  function example() {                                   β”‚
β”‚      // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚      // β”‚     TEMPORAL DEAD ZONE for 'x'          β”‚    β”‚
β”‚      // β”‚                                          β”‚    β”‚
β”‚      // β”‚  console.log(x); // ReferenceError!      β”‚    β”‚
β”‚      // β”‚  doSomething();                          β”‚    β”‚
β”‚      // β”‚                                          β”‚    β”‚
β”‚      // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚      let x = 10; // TDZ ends, 'x' is now accessible    β”‚
β”‚      console.log(x); // 10                              β”‚
β”‚  }                                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why TDZ Exists:

TDZ prevents the confusing behavior of var:

// With var (confusing)
console.log(x); // undefined - exists but no value
var x = 10;

// With let (clearer error)
console.log(y); // ReferenceError - clearly an error
let y = 10;

Function Expressions Are NOT Hoisted

sayHi(); // TypeError: sayHi is not a function

var sayHi = function () {
  console.log('Hi!');
};

What JavaScript Sees:

// Creation phase
var sayHi; // undefined

// Execution phase
sayHi(); // undefined() - TypeError!
sayHi = function () {
  console.log('Hi!');
};

With let/const:

sayHi(); // ReferenceError: Cannot access 'sayHi' before initialization

const sayHi = function () {
  console.log('Hi!');
};

Arrow Functions Are NOT Hoisted

Same as function expressions:

greet(); // ReferenceError (with const) or TypeError (with var)

const greet = () => {
  console.log('Hello!');
};

Hoisting Order of Precedence

When both a function and variable have the same name:

console.log(typeof foo); // "function"

var foo = "I'm a string";

function foo() {
  return "I'm a function";
}

console.log(typeof foo); // "string"

Rules:

  1. β€’Function declarations are hoisted first
  2. β€’Variable declarations are ignored if name already exists
  3. β€’Variable assignments happen during execution

Step by Step:

// 1. Creation Phase
function foo() {
  return "I'm a function";
} // Function hoisted first
// var foo ignored - 'foo' already exists

// 2. Execution Phase
console.log(typeof foo); // "function"
foo = "I'm a string"; // Assignment overwrites
console.log(typeof foo); // "string"

Class Hoisting

Classes are hoisted but remain in the TDZ:

const instance = new MyClass(); // ReferenceError!

class MyClass {
  constructor() {
    this.name = 'MyClass';
  }
}

Class Expressions:

const instance = new MyClass(); // ReferenceError or TypeError

const MyClass = class {
  constructor() {
    this.name = 'MyClass';
  }
};

Hoisting in Blocks

var Ignores Blocks:

function example() {
  if (false) {
    var x = 10; // Still hoisted to function scope!
  }
  console.log(x); // undefined (not an error)
}
example();

let/const Respect Blocks:

function example() {
  if (false) {
    let x = 10; // Block-scoped
  }
  console.log(x); // ReferenceError: x is not defined
}
example();

Hoisting in Loops

The Classic var Loop Problem:

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

Why? var i is hoisted to function scope. There's only ONE i.

The let Fix:

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

Why? let creates a new i for each iteration.


Best Practices

1. Declare Variables at the Top

// Good: Variables at top
function processUser(user) {
  const name = user.name;
  const age = user.age;
  let status = 'pending';

  // ... rest of function
}

// Avoid: Scattered declarations
function processUser(user) {
  console.log(name); // undefined (with var) or error (with let)
  // ... code ...
  var name = user.name;
  // ... more code ...
}

2. Use const by Default, let When Needed

const PI = 3.14159; // Never changes
const user = { name: 'A' }; // Reference doesn't change

let count = 0; // Will be reassigned
count++;

3. Avoid var in Modern JavaScript

// Modern JavaScript
const items = [];
let total = 0;

// Avoid
var items = [];
var total = 0;

4. Declare Functions Before Use (for readability)

// Clear and readable
function main() {
  doStep1();
  doStep2();
  doStep3();
}

function doStep1() {
  /* ... */
}
function doStep2() {
  /* ... */
}
function doStep3() {
  /* ... */
}

main();

Summary Table

DeclarationHoisted?Initial ValueAccessible Before Declaration?
function declarationYes (fully)The functionYes
varYesundefinedYes (returns undefined)
letYes (TDZ)UninitializedNo (ReferenceError)
constYes (TDZ)UninitializedNo (ReferenceError)
classYes (TDZ)UninitializedNo (ReferenceError)
Function expressionNo*N/ANo
Arrow functionNo*N/ANo

*The variable holding the expression is hoisted, but not the function itself.


Common Interview Questions

  1. β€’

    What is hoisting?

    • β€’Moving declarations to the top during the creation phase
  2. β€’

    What's the difference between var and let hoisting?

    • β€’Both are hoisted, but let is in TDZ until declaration
  3. β€’

    Why does this log undefined?

    console.log(x);
    var x = 5;
    
    • β€’var x is hoisted, but assignment isn't
  4. β€’

    What's the Temporal Dead Zone?

    • β€’The period where let/const exist but can't be accessed

Next Steps

Next, we'll explore the Event Loopβ€”how JavaScript handles asynchronous operations despite being single-threaded.

.3 Hoisting Deep Dive - JavaScript Tutorial | DeepML