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
- Function declarations: Fully hoisted (can call before declaration)
- var declarations: Hoisted and initialized with
undefined - let/const declarations: Hoisted but not initialized (TDZ)
- Function expressions: Only the variable declaration is hoisted
- Classes: Not hoisted (TDZ applies)
- 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
