Chapter 9: The Hoisting Phenomenon
· 10 min read
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