Understanding Variable and Function Hoisting
Hoisting is JavaScript's default behavior of moving declarations to the top of their scope before code execution. This means you can use variables and functions before they're declared in your code.
// This works!
console.log(greeting); // undefined
var greeting = "Hello";
// Behind the scenes, JavaScript does this:
var greeting; // Declaration hoisted
console.log(greeting); // undefined
greeting = "Hello"; // Assignment stays in place
Variables declared with var are hoisted to the top of their function or global scope
and initialized with undefined.
function example() {
console.log(x); // undefined (not ReferenceError!)
var x = 5;
console.log(x); // 5
}
// What JavaScript actually does:
function example() {
var x; // Hoisted and initialized to undefined
console.log(x); // undefined
x = 5; // Assignment
console.log(x); // 5
}
var name = "Global";
function showName() {
console.log(name); // undefined (not "Global"!)
var name = "Local";
console.log(name); // "Local"
}
// Why? Because JavaScript does this:
function showName() {
var name; // Local var hoisted, shadows global
console.log(name); // undefined
name = "Local";
console.log(name); // "Local"
}
Variables declared with let and const are also hoisted, but they're
not initialized. Accessing them before declaration causes a ReferenceError.
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
const y = 10;
The TDZ is the period between entering scope and the variable declaration being executed. During this time, the variable exists but cannot be accessed.
// TDZ starts here for 'name'
console.log(name); // ReferenceError
let name = "Alice"; // TDZ ends here
console.log(name); // "Alice"
function example() {
// TDZ for 'temp' starts
console.log(temp); // ReferenceError
let temp = 5; // TDZ ends
console.log(temp); // 5
}
Function declarations are fully hoisted - both the declaration and the definition. You can call them before they appear in your code.
// This works!
greet(); // "Hello!"
function greet() {
console.log("Hello!");
}
// JavaScript hoists the entire function
function greet() {
console.log("Hello!");
}
greet(); // "Hello!"
Function expressions (functions assigned to variables) follow variable hoisting rules.
// Using var
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
// What JavaScript does:
var sayHi; // Hoisted, initialized to undefined
sayHi(); // TypeError: undefined is not a function
sayHi = function() {
console.log("Hi!");
};
// Using let/const
sayHello(); // ReferenceError: Cannot access 'sayHello' before initialization
const sayHello = function() {
console.log("Hello!");
};
Arrow functions follow the same hoisting rules as function expressions.
// ReferenceError
multiply(2, 3);
const multiply = (a, b) => a * b;
Classes are hoisted but not initialized, similar to let and const.
const dog = new Dog(); // ReferenceError
class Dog {
constructor(name) {
this.name = name;
}
}
// With var - common bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (var is hoisted to function scope)
// With let - works as expected
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 (let is block-scoped)
// Avoid this - behavior varies across browsers
if (true) {
function foo() {
return "A";
}
} else {
function foo() {
return "B";
}
}
// Use function expressions instead
let foo;
if (true) {
foo = function() {
return "A";
};
} else {
foo = function() {
return "B";
};
}
let x = 10;
function test() {
console.log(x); // ReferenceError (not 10!)
let x = 20; // This shadows the outer x
}
// The let x inside test() is hoisted but in TDZ
// Good
function calculate() {
let result = 0;
let temp = 5;
// Use variables
result = temp * 2;
return result;
}
// Good
const PI = 3.14159;
let counter = 0;
// Avoid var
// var x = 5; // Don't use var in modern JavaScript
// Good - clear and predictable
const greet = function(name) {
return `Hello, ${name}!`;
};
console.log(greet("Alice"));
// Function declarations are fine at the top level
function initialize() {
// Setup code
}
function processData(data) {
// Processing logic
}
// Call them
initialize();
processData(myData);
// Wrong assumption: "let doesn't hoist"
let x = 10;
function test() {
console.log(x); // ReferenceError (not 10!)
let x = 20; // This IS hoisted, but in TDZ
}
// Bad
function process() {
result = calculate(); // What's result?
var result; // Declared too late
return result;
}
// Good
function process() {
let result = calculate();
return result;
}
// Works but confusing
doSomething();
function doSomething() {
// Code
}
// Better - clear order
function doSomething() {
// Code
}
doSomething();
var a = 1;
function test() {
console.log(a);
var a = 2;
}
test();
Answer: undefined - The local var a is hoisted and shadows the global a.
console.log(foo());
function foo() {
return "A";
}
var foo = function() {
return "B";
};
Answer: "A" - Function declaration is hoisted above the variable assignment.
let x = 1;
{
console.log(x);
let x = 2;
}
Answer: ReferenceError - The inner let x is hoisted to the block scope but in TDZ.
undefinedconst and let over varUnderstanding hoisting helps you avoid subtle bugs and write more predictable JavaScript code. While hoisting is a core JavaScript behavior, modern best practices encourage writing code that doesn't rely on it.
Practice tip: Review your existing code for hoisting-related issues. Replace
var with const or let, and declare all variables at the
top of their scope.