Back to Articles

Chapter 9: The Hoisting Phenomenon

July 27, 202510 min read
javascripthoistingtemporal-dead-zonetdzvariable-hoistingfunction-hoistinginterviews
Chapter 9: The Hoisting Phenomenon

Chapter 9: The Hoisting Phenomenon

Hoisting is one of JavaScript's most misunderstood concepts. Think of it as JavaScript's way of "preparing" your code before execution.

The Hoisting Hierarchy

// What you write:
console.log(hoistedVar);    // undefined (not an error!)
console.log(hoistedFunc());  // "Hello from hoisted function!"
// console.log(notHoistedLet); // ReferenceError: Cannot access before initialization
// console.log(notHoistedConst); // ReferenceError: Cannot access before initialization

var hoistedVar = "I'm hoisted!";

function hoistedFunc() {
    return "Hello from hoisted function!";
}

let notHoistedLet = "I'm in TDZ until this line";
const notHoistedConst = "I'm also in TDZ";

// What JavaScript actually does (conceptually):
var hoistedVar; // Hoisted and initialized with undefined

function hoistedFunc() { // Completely hoisted
    return "Hello from hoisted function!";
}

// let and const are hoisted but not initialized (TDZ)

console.log(hoistedVar);    // undefined
console.log(hoistedFunc()); // "Hello from hoisted function!"

hoistedVar = "I'm hoisted!";
let notHoistedLet = "I'm in TDZ until this line";
const notHoistedConst = "I'm also in TDZ";

Function Hoisting vs Function Expression Hoisting

// Function declaration - fully hoisted
console.log(declaredFunction()); // "I'm declared!" - works!

function declaredFunction() {
    return "I'm declared!";
}

// Function expression - only variable is hoisted
console.log(typeof expressedFunction); // "undefined"
// console.log(expressedFunction());    // TypeError: expressedFunction is not a function

var expressedFunction = function() {
    return "I'm expressed!";
};

// Arrow function - same as function expression
// console.log(arrowFunction());        // TypeError: arrowFunction is not a function
var arrowFunction = () => "I'm an arrow!";

// With let/const - even worse!
// console.log(letFunction());          // ReferenceError: Cannot access before initialization
let letFunction = function() {
    return "I'm let!";
};

The Temporal Dead Zone (TDZ) in Detail

console.log("Starting execution");

// TDZ starts here for 'letVar' and 'constVar'
console.log(typeof letVar);   // ReferenceError - TDZ violation
console.log(typeof constVar); // ReferenceError - TDZ violation

let letVar = "Now I'm alive!";
const constVar = "Me too!";
// TDZ ends here

// Interesting TDZ behavior
function tdz_example() {
    console.log(typeof x); // "undefined" - x doesn't exist yet
    
    if (true) {
        // TDZ for 'x' starts here
        console.log(typeof x); // ReferenceError - x is in TDZ
        let x = "I'm block scoped";
        // TDZ for 'x' ends here
    }
}

Hoisting in Different Contexts

Class Hoisting

// ❌ Classes are NOT hoisted like functions
// const myInstance = new MyClass(); // ReferenceError

class MyClass {
    constructor() {
        this.value = 42;
    }
}

const myInstance = new MyClass(); // ✅ Works after declaration

// Class expressions follow the same rules as function expressions
// const instance = new MyClassExpr(); // ReferenceError

const MyClassExpr = class {
    constructor() {
        this.value = 42;
    }
};

Import Hoisting

// Imports are hoisted to the top of the module
console.log(myFunction()); // Works! Imports are hoisted

import { myFunction } from './module.js';

// But the module's code executes in order
// If module.js has side effects, they happen before the console.log

Common Hoisting Pitfalls and Interview Questions

The Classic Interview Question

// What will this output?
var x = 1;

function foo() {
    console.log(x); // undefined (not 1!)
    var x = 2;
}

foo();

// Explanation: The local var x is hoisted within foo
// It shadows the global x, but is undefined until assigned

Hoisting with Loops

// The infamous loop variable problem
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// Why? var is function-scoped and hoisted
// By the time setTimeout runs, i = 3

// Solution with let (block-scoped)
for (let j = 0; j < 3; j++) {
    setTimeout(() => console.log(j), 100); // 0, 1, 2
}

Function vs Variable Precedence

// When both have the same name
console.log(typeof foo); // "function"

var foo = "I'm a variable";

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

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

// Functions are hoisted first, then variables
// But variable assignments happen in code order

Practical Implications of Hoisting

Best Practices to Avoid Hoisting Issues

// 1. Declare variables at the top of their scope
function goodPractice() {
    let result;
    let temp;
    
    // Use variables after declaration
    result = calculateSomething();
    temp = result * 2;
    
    return temp;
}

// 2. Use const by default, let when needed, avoid var
const API_KEY = "secret"; // Can't be reassigned
let userScore = 0;        // Can be updated
// var oldWay = "avoid";  // Don't use var

// 3. Define functions before calling them (even though hoisting allows otherwise)
// Makes code more readable and predictable

// 4. Be aware of the TDZ
function checkTDZ() {
    // console.log(value); // Would throw ReferenceError
    let value = getTDZSafeValue();
    console.log(value); // Safe to use here
}

Hoisting and Closures Interaction

function createFunctions() {
    var functions = [];
    
    for (var i = 0; i < 3; i++) {
        // var is hoisted to function scope
        functions.push(function() {
            console.log(i); // All will log 3
        });
    }
    
    return functions;
}

const funcs = createFunctions();
funcs[0](); // 3 (not 0!)
funcs[1](); // 3 (not 1!)
funcs[2](); // 3 (not 2!)

// Fixed version with let
function createFunctionsFixed() {
    const functions = [];
    
    for (let i = 0; i < 3; i++) {
        // let is block-scoped
        functions.push(function() {
            console.log(i); // Captures the correct value
        });
    }
    
    return functions;
}

Edge Cases and Gotchas

// Hoisting doesn't cross function boundaries
function outer() {
    console.log(inner); // ReferenceError - inner is not defined
    
    function nested() {
        function inner() {
            return "I'm deeply nested";
        }
    }
}

// Default parameters and TDZ
function weirdTDZ(a = b, b = 1) {
    // TDZ error! b is not initialized when a tries to use it
    return a + b;
}

// weirdTDZ(); // ReferenceError

// This works fine
function normalParams(a = 1, b = a) {
    return a + b;
}

console.log(normalParams()); // 2

Summary: Hoisting Rules

  1. Function declarations: Fully hoisted (can call before declaration)
  2. var declarations: Hoisted and initialized with undefined
  3. let/const declarations: Hoisted but not initialized (TDZ)
  4. Function expressions: Only the variable declaration is hoisted
  5. Classes: Not hoisted (TDZ applies)
  6. Imports: Hoisted to the top of the module

Remember: Just because JavaScript allows something doesn't mean you should do it. Write clear, predictable code by declaring before using!


Next Chapter: Chapter 10: The Asynchronous Adventures

Previous Chapter: Chapter 8: The Scope and Closure Mysteries

Table of Contents: JavaScript Guide