JavaScript Hoisting: Understanding Variable and Function Lifting
Master JavaScript hoisting behavior. Learn how variables, functions, and classes are hoisted, common pitfalls, and best practices for avoiding hoisting-related bugs.
Hoisting is JavaScript's default behavior of moving declarations to the top of their containing scope during the compilation phase. Understanding hoisting is crucial for avoiding bugs and writing predictable JavaScript code.
What is Hoisting?
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution. This means you can use functions and variables before they're declared in your code.
Basic Example of Hoisting
// This works due to hoisting
console.log(greet()); // "Hello, World!"
function greet() {
return 'Hello, World!';
}
// But this doesn't work as expected
console.log(myVar); // undefined (not ReferenceError)
var myVar = 5;
console.log(myVar); // 5
// What JavaScript actually sees:
// var myVar; // Declaration is hoisted
// console.log(myVar); // undefined
// myVar = 5; // Assignment stays in place
// console.log(myVar); // 5
Function Hoisting
Function declarations are fully hoisted, meaning both the declaration and the function body are moved to the top.
Function Declarations
// You can call the function before it's declared
sayHello(); // "Hello!"
function sayHello() {
console.log('Hello!');
}
// Multiple function calls before declaration
console.log(add(5, 3)); // 8
console.log(multiply(4, 2)); // 8
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// Nested function hoisting
function outer() {
inner(); // Works!
function inner() {
console.log('Inner function');
}
}
outer();
Function Expressions
Function expressions are not hoisted the same way as function declarations.
// This throws an error
// sayHi(); // TypeError: sayHi is not a function
var sayHi = function () {
console.log('Hi!');
};
sayHi(); // "Hi!" - Works after assignment
// With const or let
// greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = function () {
console.log('Greetings!');
};
// Arrow functions behave like function expressions
// welcome(); // ReferenceError
const welcome = () => {
console.log('Welcome!');
};
Named Function Expressions
// The variable is hoisted, but not the function
console.log(typeof myFunc); // undefined
var myFunc = function namedFunc() {
console.log('Named function expression');
// 'namedFunc' is only available inside the function
console.log(typeof namedFunc); // "function"
};
myFunc(); // "Named function expression"
// namedFunc(); // ReferenceError: namedFunc is not defined
Variable Hoisting
Variable hoisting behaves differently for var
, let
, and const
.
var Hoisting
Variables declared with var
are hoisted and initialized with undefined
.
console.log(x); // undefined (not ReferenceError)
var x = 5;
console.log(x); // 5
// Multiple var declarations
console.log(a, b, c); // undefined undefined undefined
var a = 1;
var b = 2;
var c = 3;
console.log(a, b, c); // 1 2 3
// In functions
function example() {
console.log(localVar); // undefined
var localVar = "I'm local";
console.log(localVar); // "I'm local"
}
example();
// var in loops
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3 3 3
}
// 'i' is hoisted to function scope
let and const Hoisting (Temporal Dead Zone)
let
and const
are hoisted but not initialized, creating a "Temporal Dead Zone".
// Temporal Dead Zone example
// console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = 5;
console.log(myLet); // 5
// console.log(myConst); // ReferenceError: Cannot access 'myConst' before initialization
const myConst = 10;
console.log(myConst); // 10
// The Temporal Dead Zone in action
let x = 'outer';
function example() {
// TDZ starts
// console.log(x); // ReferenceError
// TDZ ends
let x = 'inner';
console.log(x); // "inner"
}
example();
// Block scope with let/const
{
// console.log(blockVar); // ReferenceError
let blockVar = "I'm in a block";
console.log(blockVar); // "I'm in a block"
}
// console.log(blockVar); // ReferenceError: blockVar is not defined
Temporal Dead Zone Explained
// TDZ visualization
function demonstrateTDZ() {
// TDZ for 'a' starts here
console.log('Function starts');
// Accessing 'a' here would throw ReferenceError
// console.log(a);
console.log('Before declaration');
let a = 5; // TDZ for 'a' ends here
console.log(a); // 5 - Safe to access
}
// TDZ with const
function constTDZ() {
// const b; // SyntaxError: Missing initializer
const b = 10; // Must be initialized
console.log(b);
}
// TDZ in switch statements
function switchTDZ(value) {
switch (value) {
case 1:
// console.log(x); // ReferenceError if case 1 executes
let x = 'case 1';
console.log(x);
break;
case 2:
// x is not accessible here
let y = 'case 2';
console.log(y);
break;
}
}
Class Hoisting
Classes are hoisted but remain in the Temporal Dead Zone until declaration.
// const myInstance = new MyClass(); // ReferenceError
class MyClass {
constructor() {
this.name = 'MyClass';
}
}
const myInstance = new MyClass(); // Works after declaration
console.log(myInstance.name); // "MyClass"
// Class expressions
// const instance = new MyClassExpr(); // ReferenceError
const MyClassExpr = class {
constructor() {
this.type = 'expression';
}
};
// Function vs Class hoisting difference
greet(); // Works - function hoisting
function greet() {
console.log('Hello from function');
}
// new Greeting(); // ReferenceError - class not hoisted
class Greeting {
sayHello() {
console.log('Hello from class');
}
}
Hoisting in Different Scopes
Global Scope Hoisting
// Global scope
console.log(globalVar); // undefined
var globalVar = "I'm global";
// Function declarations in global scope
console.log(typeof globalFunc); // "function"
function globalFunc() {
return 'Global function';
}
// Window object (in browsers)
console.log(window.globalVar); // undefined initially
console.log(window.globalFunc); // function globalFunc()
Function Scope Hoisting
function outerFunction() {
// All declarations are hoisted to the top of the function
console.log(innerVar); // undefined
console.log(innerFunc()); // "Inner function result"
var innerVar = 'Inner variable';
function innerFunc() {
return 'Inner function result';
}
// Nested function scope
function nestedExample() {
console.log(deepVar); // undefined
var deepVar = 'Deeply nested';
// 'innerVar' is accessible from parent scope
console.log(innerVar); // "Inner variable"
}
nestedExample();
}
outerFunction();
Block Scope Hoisting
// Block scope with let and const
{
// console.log(blockLet); // ReferenceError
let blockLet = 'Block scoped with let';
const blockConst = 'Block scoped with const';
// var ignores block scope
var blockVar = 'Not block scoped';
}
console.log(blockVar); // "Not block scoped"
// console.log(blockLet); // ReferenceError
// console.log(blockConst); // ReferenceError
// In if statements
if (true) {
// console.log(ifLet); // ReferenceError
let ifLet = 'Inside if';
var ifVar = 'Also inside if';
}
console.log(ifVar); // "Also inside if"
// console.log(ifLet); // ReferenceError
Common Hoisting Pitfalls
The Classic Loop Problem
// Problem with var in loops
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // Prints 5, 5, 5, 5, 5
}, 100);
}
// Solution 1: Use let
for (let j = 0; j < 5; j++) {
setTimeout(function () {
console.log(j); // Prints 0, 1, 2, 3, 4
}, 100);
}
// Solution 2: IIFE (before let/const)
for (var k = 0; k < 5; k++) {
(function (index) {
setTimeout(function () {
console.log(index); // Prints 0, 1, 2, 3, 4
}, 100);
})(k);
}
// Solution 3: Function factory
function createLogger(index) {
return function () {
console.log(index);
};
}
for (var m = 0; m < 5; m++) {
setTimeout(createLogger(m), 100);
}
Variable Shadowing
var x = 1;
function shadowExample() {
console.log(x); // undefined (not 1!)
var x = 2; // This shadows the outer x
console.log(x); // 2
}
shadowExample();
console.log(x); // 1
// With let - throws error
let y = 1;
function letShadow() {
// console.log(y); // ReferenceError
let y = 2;
console.log(y); // 2
}
letShadow();
Function Name Conflicts
// Function overwriting
function foo() {
return 'First foo';
}
function foo() {
return 'Second foo'; // This overwrites the first
}
console.log(foo()); // "Second foo"
// Variable and function name conflicts
function bar() {
return "I'm a function";
}
var bar = "I'm a variable"; // Overwrites the function
console.log(bar); // "I'm a variable"
// bar(); // TypeError: bar is not a function
Best Practices
1. Declare Variables at the Top
// Good practice
function goodExample() {
let a, b, c;
const MAX_SIZE = 100;
// Use variables after declaration
a = 5;
b = 10;
c = a + b;
return c;
}
// Avoid scattered declarations
function avoidThis() {
console.log(someLogic());
var x = 5; // Declared in the middle
function someLogic() {
return x; // undefined when called above
}
}
2. Use const and let Instead of var
// Modern approach
const CONFIG = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
let userCount = 0;
function incrementUser() {
userCount++;
}
// Avoid var
// var config = {}; // Can be reassigned and hoisted
// var count = 0; // Function-scoped, not block-scoped
3. Initialize Variables When Declaring
// Good
const userName = 'John';
let isLoggedIn = false;
let currentScore = 0;
// Avoid
let userName; // undefined
let isLoggedIn;
userName = 'John';
isLoggedIn = false;
4. Place Functions Before Calls (Even Though Hoisted)
// Readable and clear
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
// Use functions after declaration
const items = [{ price: 10 }, { price: 20 }];
const total = calculateTotal(items);
console.log(formatCurrency(total));
Advanced Hoisting Concepts
Hoisting in Strict Mode
'use strict';
// Still works the same for functions
console.log(strictFunc()); // "Strict function"
function strictFunc() {
return 'Strict function';
}
// But prevents some bad practices
// x = 5; // ReferenceError: x is not defined
// Must declare variables
let x = 5;
// No hoisting for undeclared variables
function nonStrictExample() {
// Without strict mode, creates global variable
// undeclaredVar = 10;
}
Module Scope Hoisting
// In ES6 modules
// import statements are hoisted
console.log(myImport); // Works if myImport is exported
import { myImport } from './module.js';
// But the module must be evaluated first
// Circular dependencies can cause issues
// Export hoisting
export function exportedFunc() {
return "I'm exported";
}
// Can be used before in the same module
console.log(exportedFunc());
Dynamic Import and Hoisting
// Dynamic imports are not hoisted
async function loadModule() {
// This is not hoisted
const module = await import('./dynamic-module.js');
console.log(module.default);
}
// Regular imports are hoisted
import staticModule from './static-module.js';
console.log(staticModule); // Available immediately
Debugging Hoisting Issues
// Use typeof to check if variable is declared
if (typeof myVariable !== 'undefined') {
console.log(myVariable);
}
// Check for temporal dead zone
try {
console.log(myLetVar);
} catch (e) {
console.log('Variable is in TDZ:', e.message);
}
let myLetVar = 5;
// Use debugger to inspect hoisting
function debugHoisting() {
debugger; // Pause here to inspect scope
console.log(a); // undefined
var a = 5;
console.log(a); // 5
}
// Console logging for understanding execution order
console.log('1. Start of script');
console.log('2. typeof myFunc:', typeof myFunc);
console.log('3. typeof myVar:', typeof myVar);
function myFunc() {
console.log('4. Inside myFunc');
}
var myVar = 'value';
console.log('5. End of script');
Conclusion
Hoisting is a fundamental JavaScript behavior that can lead to confusion if not properly understood. While function declarations are fully hoisted and can be used before their declaration, variables declared with var
are hoisted but initialized with undefined
, and let
/const
declarations remain in a temporal dead zone until their declaration is reached. Understanding these behaviors helps write more predictable code and avoid common pitfalls. Following best practices like using const
and let
, declaring variables at the top of their scope, and being explicit about initialization will lead to cleaner, more maintainable JavaScript code.