JavaScript Hoisting
Understanding Declaration Hoisting
Explanation
What is Hoisting?
Hoisting is JavaScript's default behavior of moving declarations to the top of their scope before code execution. Only declarations are hoisted, not initializations.
Key Concepts
- var declarations: Hoisted and initialized to
undefined - let/const declarations: Hoisted but not initialized (TDZ)
- Function declarations: Fully hoisted
- Function expressions: Only variable hoisted
Temporal Dead Zone (TDZ)
The TDZ is the time between entering scope and variable declaration where let and const cannot be accessed.
Demonstration
Example 1: var Hoisting
// What you write
console.log(name); // undefined (not an error!)
var name = 'Arthur';
console.log(name); // 'Arthur'
// How JavaScript interprets it
var name; // Declaration hoisted
console.log(name); // undefined
name = 'Arthur'; // Initialization stays
console.log(name); // 'Arthur'
// Multiple var declarations
var x = 1;
var x = 2; // No error, just reassigns
console.log(x); // 2
// var in function scope
function example() {
console.log(value); // undefined
var value = 10;
console.log(value); // 10
}
// var ignores block scope
if (true) {
var blockVar = 'I escape blocks!';
}
console.log(blockVar); // 'I escape blocks!'
Example 2: let and const Hoisting (TDZ)
// let - hoisted but not initialized
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 30;
// The variable IS hoisted (proven below)
let x = 'outer';
function checkTDZ() {
console.log(x); // ReferenceError, not 'outer'!
let x = 'inner'; // TDZ starts at function entry
}
// const behaves the same
console.log(PI); // ReferenceError
const PI = 3.14159;
// TDZ with typeof
console.log(typeof undeclared); // 'undefined' (no error)
console.log(typeof inTDZ); // ReferenceError
let inTDZ = 'value';
// TDZ in switch statements
switch (x) {
case 0:
let foo; // TDZ covers entire switch block
break;
case 1:
foo = 1; // OK, after declaration
break;
case 2:
let foo; // SyntaxError: already declared
break;
}
// TDZ with default parameters
function example(a = b, b = 1) { // ReferenceError
// 'b' is in TDZ when 'a' default is evaluated
}
// Safe pattern
function safe(a = 1, b = a) { // OK, 'a' is already initialized
console.log(a, b); // 1, 1
}
Example 3: Function Hoisting
// Function declarations are fully hoisted
sayHello(); // 'Hello!' - works!
function sayHello() {
console.log('Hello!');
}
// Function expressions are NOT fully hoisted
sayGoodbye(); // TypeError: sayGoodbye is not a function
var sayGoodbye = function() {
console.log('Goodbye!');
};
// What actually happens with function expression
var sayGoodbye; // Declaration hoisted
sayGoodbye(); // undefined() - TypeError!
sayGoodbye = function() { ... }; // Assignment stays
// Arrow functions same as function expressions
greet(); // TypeError: greet is not a function
var greet = () => console.log('Hi!');
// Function declarations beat var
console.log(myFunc); // [Function: myFunc]
var myFunc = 'string';
function myFunc() {}
console.log(myFunc); // 'string'
// Order of hoisting
// 1. Function declarations (fully)
// 2. var declarations (undefined)
// 3. Code executes in order
Example 4: Hoisting in Loops
// var in loops - common gotcha
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (not 0, 1, 2!)
// 'i' is hoisted to function scope, shared by all callbacks
// Fix with let (block-scoped)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Output: 0, 1, 2 (each iteration gets its own 'j')
// Fix with IIFE (old pattern)
for (var k = 0; k < 3; k++) {
(function(k) {
setTimeout(() => console.log(k), 100);
})(k);
}
// Output: 0, 1, 2
// var leaks out of for loop
for (var index = 0; index < 5; index++) {}
console.log(index); // 5 - still accessible!
// let stays in block
for (let idx = 0; idx < 5; idx++) {}
console.log(idx); // ReferenceError: idx is not defined
Example 5: Class Hoisting
// Classes are NOT hoisted like functions
const instance = new MyClass(); // ReferenceError
class MyClass {
constructor() {
this.name = 'MyClass';
}
}
// Class expressions - same behavior
const obj = new MyOtherClass(); // ReferenceError
const MyOtherClass = class {
constructor() {}
};
// Why? Classes have a TDZ like let/const
// This prevents accessing before full definition
// Safe pattern - declare before use
class Animal {
speak() {
return 'sound';
}
}
class Dog extends Animal {
speak() {
return 'woof';
}
}
const dog = new Dog();
console.log(dog.speak()); // 'woof'
Example 6: Practical Patterns
// Module pattern leveraging hoisting
const calculator = (function() {
// Private functions available due to hoisting
return {
add: (a, b) => add(a, b),
multiply: (a, b) => multiply(a, b)
};
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
})();
calculator.add(2, 3); // 5
// Avoiding hoisting issues - best practices
// 1. Use const/let instead of var
// 2. Declare variables at the top of their scope
// 3. Declare functions before calling them
// 4. Use strict mode
'use strict';
// This helps catch hoisting-related mistakes
function strictExample() {
x = 10; // ReferenceError in strict mode
var x;
}
// ESLint rules to enforce
// "no-use-before-define": "error"
// "no-var": "error"
// "prefer-const": "error"
Key Takeaways:
- var is hoisted and initialized to undefined
- let/const are hoisted but stay in TDZ
- Function declarations are fully hoisted
- Function expressions/arrows follow variable rules
- Use let/const to avoid hoisting confusion
Imitation
Challenge 1: Predict the Output
Task: Determine what each console.log will output.
var a = 1;
function outer() {
console.log(a); // ?
var a = 2;
function inner() {
console.log(a); // ?
var a = 3;
console.log(a); // ?
}
inner();
console.log(a); // ?
}
outer();
console.log(a); // ?
Solution
var a = 1;
function outer() {
console.log(a); // undefined (local 'a' hoisted)
var a = 2;
function inner() {
console.log(a); // undefined (inner 'a' hoisted)
var a = 3;
console.log(a); // 3
}
inner();
console.log(a); // 2 (outer's 'a')
}
outer();
console.log(a); // 1 (global 'a')
// Explanation:
// Each function creates its own scope
// var declarations are hoisted to the top of their function
// The hoisted variable shadows any outer variables
Practice
Exercise 1: Fix the Loop
Difficulty: Beginner
Fix this code so it logs 0, 1, 2:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Exercise 2: Hoisting Quiz
Difficulty: Intermediate
Predict and explain the output:
console.log(foo);
console.log(bar);
console.log(baz);
var foo = 'foo';
let bar = 'bar';
function baz() { return 'baz'; }
Summary
What you learned:
- How var, let, and const are hoisted differently
- Temporal Dead Zone (TDZ)
- Function declaration vs expression hoisting
- Common hoisting pitfalls
- Best practices to avoid issues
Next Steps:
- Read: Scope
- Practice: Refactor var to let/const
- Deep dive: Module systems
