JavaScript Closures Explained: A Practical Guide with Real-World Examples
Master JavaScript closures with clear explanations and practical examples. Learn how closures work, why they matter, and how to use them in your daily coding.
JavaScript Closures: The Definitive Guide with Real Examples
Closures are one of those JavaScript concepts that developers either claim to understand or admit they find confusing. The truth is that closures are not complicated. You have probably used them many times without realizing it. This guide will demystify closures completely with practical examples you can use tomorrow.
What Exactly Is a Closure?
Let us start with the simplest possible definition. A closure is a function that remembers the variables from its surrounding scope even after that surrounding scope has finished executing.
The one-sentence answer: A closure gives you access to an outer function's scope from an inner function.
The Simplest Example
function outerFunction() {
let message = "Hello from outer!";
function innerFunction() {
console.log(message);
}
return innerFunction;
}
const myClosure = outerFunction();
myClosure(); // Output: Hello from outer!
Look at what happened here. The outerFunction finished running. It returned the innerFunction and then... ended. Normally we would expect the message variable to be gone. But when we called myClosure(), it still remembered the message. That is the closure at work.
The inner function closed over the variables from its outer scope, keeping them alive even after the outer function returned.
A Slightly More Useful Example
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
const anotherCounter = createCounter();
console.log(anotherCounter()); // 1 (independent counter)
Each counter has its own private count variable that is preserved between calls. The count is not accessible from outside. Only the returned function can see and change it. This is a closure in action.
Lexical Scoping: The Foundation of Closures
To understand closures, you first need to understand lexical scoping. Lexical scoping means that a function's access to variables is determined by where the function is written in the code, not by where it is called from.
What Lexical Scoping Looks Like
const globalVar = "I am global";
function outer() {
const outerVar = "I am in outer";
function inner() {
const innerVar = "I am in inner";
console.log(globalVar); // Accessible (global)
console.log(outerVar); // Accessible (outer function)
console.log(innerVar); // Accessible (own scope)
}
inner();
// console.log(innerVar); // ERROR - innerVar not accessible here
}
outer();
// console.log(outerVar); // ERROR - outerVar not accessible here
The key point: inner functions can see variables from all outer scopes. But outer functions cannot see variables from inner scopes. This is lexical scoping.
The Scope Chain
const level1 = "global";
function functionA() {
const level2 = "functionA";
function functionB() {
const level3 = "functionB";
function functionC() {
const level4 = "functionC";
// functionC can access level1, level2, level3, level4
console.log(level1, level2, level3, level4);
}
// functionB can access level1, level2, level3
// but NOT level4
console.log(level1, level2, level3);
functionC();
}
// functionA can access level1, level2
// but NOT level3 or level4
console.log(level1, level2);
functionB();
}
functionA();
Each function looks for variables in its own scope first. If it does not find them, it looks up to the parent scope, then to the grandparent, all the way up to the global scope. This is called the scope chain.
How Closures Work Under the Hood
When a function is created in JavaScript, it gets a hidden property called [[Environment]] that stores a reference to the lexical environment where the function was created. This reference survives even after the outer function finishes.
function makeGreeting(language) {
return function(firstName, lastName) {
if (language === 'en') {
console.log(`Hello ${firstName} ${lastName}`);
}
if (language === 'es') {
console.log(`Hola ${firstName} ${lastName}`);
}
};
}
const greetEnglish = makeGreeting('en');
const greetSpanish = makeGreeting('es');
greetEnglish('John', 'Doe'); // Hello John Doe
greetSpanish('Juan', 'Perez'); // Hola Juan Perez
Each returned function remembers its own language value. When makeGreeting finished, you might think the language parameter would be gone. But each closure preserved its own copy of the environment.
When makeGreeting('en') is called:
Creates an environment where language = 'en'
Returns the inner function with that environment attached
When makeGreeting('es') is called:
Creates a separate environment where language = 'es'
Returns a different function with its own environment
Visualizing Closure Memory
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount > balance) {
return "Insufficient funds";
}
balance -= amount;
return balance;
},
getBalance: function() {
return balance;
}
};
}
const myAccount = createBankAccount(100);
console.log(myAccount.getBalance()); // 100
console.log(myAccount.deposit(50)); // 150
console.log(myAccount.withdraw(30)); // 120
// console.log(myAccount.balance); // undefined - balance is private
All three methods (deposit, withdraw, getBalance) share access to the same balance variable. They form a closure over that variable. The variable is not exposed directly to the outside world, but the methods can see and modify it.
Common Use Cases You Already Use
You have probably written closures many times without realizing it. Here are common patterns that rely on closures.
Event Listeners and Callbacks
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener('click', function() {
// This callback function closes over the 'message' variable
console.log(`Button clicked: ${message}`);
alert(message);
});
}
// Even after setupButton finishes, the callbacks remember their messages
setupButton('btn1', 'Hello from button 1');
setupButton('btn2', 'Hello from button 2');
Each callback function remembers its own message value. The event listener system calls these callbacks later, but the closures preserve the messages.
setTimeout and setInterval
function delayedGreeting(name, delay) {
setTimeout(function() {
console.log(`Hello ${name}!`);
}, delay);
}
delayedGreeting('Alice', 1000);
delayedGreeting('Bob', 1500);
// Output after 1 second: Hello Alice!
// Output after 1.5 seconds: Hello Bob!
By the time the setTimeout callback runs, the delayedGreeting function has long finished. Yet the callback still remembers the name value. That is a closure.
Array Methods with Callbacks
function createMultiplier(multiplier) {
return function(value) {
return value * multiplier;
};
}
const numbers = [1, 2, 3, 4, 5];
const double = createMultiplier(2);
const triple = createMultiplier(3);
const doubled = numbers.map(double);
const tripled = numbers.map(triple);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(tripled); // [3, 6, 9, 12, 15]
The map method receives a callback. Each callback closes over its own multiplier value.
Creating Private Variables with Closures
Before ES6 classes and private fields, closures were the primary way to create private variables in JavaScript. This pattern is still useful today.
The Module Pattern
const userManager = (function() {
// Private variables - cannot be accessed from outside
let users = [];
let nextId = 1;
// Private helper function
function findUserIndex(id) {
return users.findIndex(user => user.id === id);
}
// Public API
return {
addUser: function(name, email) {
const newUser = {
id: nextId++,
name: name,
email: email,
createdAt: new Date()
};
users.push(newUser);
return newUser;
},
getUser: function(id) {
const index = findUserIndex(id);
if (index === -1) return null;
return { ...users[index] }; // Return a copy
},
getAllUsers: function() {
return [...users]; // Return a copy
},
updateUser: function(id, updates) {
const index = findUserIndex(id);
if (index === -1) return false;
users[index] = { ...users[index], ...updates };
return true;
},
deleteUser: function(id) {
const index = findUserIndex(id);
if (index === -1) return false;
users.splice(index, 1);
return true;
},
getUserCount: function() {
return users.length;
}
};
})();
// Usage
userManager.addUser('Alice', 'alice@example.com');
userManager.addUser('Bob', 'bob@example.com');
console.log(userManager.getUserCount()); // 2
console.log(userManager.getAllUsers());
// console.log(userManager.users); // undefined - users is private!
The immediately-invoked function expression (IIFE) runs once and returns an object with methods. Those methods close over the private users array, giving them access while keeping it hidden from the outside world.
A More Modern Module Example
function createTodoList() {
let todos = [];
let id = 0;
return {
add: function(text) {
const todo = { id: ++id, text: text, completed: false };
todos.push(todo);
return todo;
},
complete: function(id) {
const todo = todos.find(t => t.id === id);
if (todo) todo.completed = true;
},
remove: function(id) {
todos = todos.filter(t => t.id !== id);
},
list: function(filter = 'all') {
if (filter === 'active') {
return todos.filter(t => !t.completed);
}
if (filter === 'completed') {
return todos.filter(t => t.completed);
}
return [...todos];
},
clearCompleted: function() {
todos = todos.filter(t => !t.completed);
}
};
}
const myTodos = createTodoList();
myTodos.add('Learn closures');
myTodos.add('Build a project');
myTodos.complete(1);
console.log(myTodos.list('active')); // Only incomplete tasks
console.log(myTodos.list('completed')); // Only completed tasks
Each todo list created with createTodoList() has its own private todos array. The methods close over their respective arrays, providing encapsulation.
The Famous Loop Problem and Solutions
This is a classic JavaScript interview question. Understanding why it happens and how to fix it reveals a deep understanding of closures.
The Problem
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Expected output: 1, 2, 3
// Actual output: 4, 4, 4 (or 3, 3, 3 depending on when you run it)
Why does this happen? The setTimeout callbacks run after the loop has finished. By then, the variable i (declared with var) has reached its final value. All three callbacks share the same i variable, so they all see the same value.
Key insight: The callbacks do not capture the value of i at the time they are created. They capture a reference to the variable i itself.
Solution 1: Use let Instead of var
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Output: 1, 2, 3 (works correctly)
The let keyword creates a new binding for each iteration of the loop. Each callback gets its own i value captured from its own iteration.
Solution 2: Create a Closure with an IIFE
for (var i = 1; i <= 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// Output: 1, 2, 3
The IIFE creates a new scope for each iteration. The variable j captures the current value of i. The setTimeout callback closes over j, not over the changing i.
Solution 3: Use a Factory Function
function createLogger(value) {
return function() {
console.log(value);
};
}
for (var i = 1; i <= 3; i++) {
setTimeout(createLogger(i), 1000);
}
// Output: 1, 2, 3
The factory function captures the current value in its own closure. Each returned function remembers its own specific value.
Performance Considerations
Closures are powerful, but they come with memory implications that you should understand.
Memory Usage
function heavyClosure() {
let largeArray = new Array(1000000).fill('data'); // 1 million items
return function() {
console.log('I have access to that large array');
// Even if you never use largeArray, it stays in memory
};
}
const closure = heavyClosure();
// The largeArray cannot be garbage collected because closure might still need it
Variables captured by a closure are kept in memory as long as the closure exists. If you accidentally capture large data structures you do not need, you can cause memory leaks.
Best Practices
// BAD: Capturing unnecessary large data
function createHandler_bad(user) {
const hugeData = fetchHugeData(); // 10MB of data
return function() {
console.log(user.name); // Only need user, not hugeData
// hugeData stays in memory unnecessarily
};
}
// GOOD: Only capture what you need
function createHandler_good(user) {
return function() {
console.log(user.name);
};
}
// EVEN BETTER: Use parameters to avoid closure when possible
function createHandler_best(userName) {
return function() {
console.log(userName);
};
}
When to Use and When to Avoid Closures
Good uses of closures:
- Data encapsulation and privacy
- Function factories and currying
- Event handlers that need context
- Callbacks that need persistent data
- Module patterns
Be careful with closures when:
- You are capturing large data unnecessarily
- Creating many closures in performance-critical loops
- Unintentionally keeping references to DOM elements (memory leaks)
Closure Memory Leaks with DOM Elements
function attachHandler() {
const element = document.getElementById('myButton');
const largeData = { /* big object */ };
element.addEventListener('click', function() {
console.log('Button clicked');
// Even though we don't use largeData, the closure captures it
// This prevents element from being garbage collected
});
}
// Better approach
function attachHandlerFixed() {
const element = document.getElementById('myButton');
const largeData = { /* big object */ };
element.addEventListener('click', function() {
console.log('Button clicked');
});
// largeData can be garbage collected after this function returns
// because the callback does not reference it
}
Advanced Closure Patterns
Function Currying with Closures
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
Currying uses closures to remember previously provided arguments and wait for the remaining ones.
Memoization (Caching) with Closures
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] === undefined) {
console.log('Computing result...');
cache[key] = fn.apply(this, args);
} else {
console.log('Returning cached result...');
}
return cache[key];
};
}
function slowFibonacci(n) {
if (n <= 1) return n;
return slowFibonacci(n - 1) + slowFibonacci(n - 2);
}
const memoizedFibonacci = memoize(slowFibonacci);
console.log(memoizedFibonacci(40)); // Computes, takes time
console.log(memoizedFibonacci(40)); // Returns cached, instant
The memoization function uses a closure over the cache object. The cache persists between calls, dramatically improving performance for repeated operations.
Once Function (Run Only Once)
function once(fn) {
let hasRun = false;
let result;
return function(...args) {
if (!hasRun) {
hasRun = true;
result = fn.apply(this, args);
}
return result;
};
}
const initialize = once(function() {
console.log('Initializing... (this runs only once)');
return { status: 'ready', timestamp: Date.now() };
});
console.log(initialize()); // Runs, logs "Initializing..."
console.log(initialize()); // Returns cached result, no log
console.log(initialize()); // Returns cached result, no log
The once function uses a closure to track whether the function has already run. This is useful for initialization code, event handlers that should fire only once, and setup operations.
Summary: What You Need to Remember
The Core Concept in One Sentence
A closure is a function that remembers and accesses variables from its outer scope even after the outer function has finished executing.
Key Takeaways
- Closures happen automatically. You do not create them. Every function in JavaScript creates a closure over its surrounding scope.
- Lexical scoping determines closure behavior. Functions look up variables in the scope where they were defined, not where they are called.
- Closures enable data privacy. You can create variables that cannot be accessed from outside, only through specific functions.
- The loop problem is a closure gotcha. Understanding why it happens helps you avoid similar issues with asynchronous code.
- Closures use memory. Variables captured by closures remain in memory as long as the closure exists.
Quick Reference: When You Are Using a Closure
- Returning a function from a function
- Passing a function as a callback (setTimeout, event listeners, array methods)
- Creating an IIFE that returns functions
- Using the module pattern
- Creating factory functions
If you are doing any of these, you are using closures.
Test Your Understanding
Before you go, try to predict what this code will output:
function testClosure() {
let x = 10;
function inner() {
console.log(x);
}
x = 20;
return inner;
}
const fn = testClosure();
fn(); // What will this output?
Answer: 20. The closure captures a reference to the variable x, not the value at the time the function was created. When x changes before the closure is returned, the closure sees the new value.
Closures are not magic. They are simply functions that carry their environment with them. Once you understand that functions in JavaScript are values that can be passed around and that they remember where they came from, closures become intuitive.
Practice writing functions that return functions. Build a counter. Create a module. Use a closure to solve a real problem in your code. The more you use closures, the more natural they become.
Discussion
Join the conversation
Login to share your thoughts with the community