Closures
Closures
Closures are one of JavaScript’s most powerful features, yet they can be tricky to understand at first. A closure gives you access to an outer function’s scope from an inner function, even after the outer function has finished executing.
The Simple Explanation
A closure is created when:
- A function is defined inside another function
- The inner function uses variables from the outer function
- The inner function is returned or passed elsewhere
The inner function “closes over” (remembers) the outer function’s variables.
Basic Example
function createGreeter(greeting) {
// 'greeting' is a variable in the outer function
return function(name) {
// This inner function has access to 'greeting'
console.log(greeting + ", " + name);
};
}
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
sayHello("Alice"); // "Hello, Alice"
sayHi("Bob"); // "Hi, Bob"
Even though createGreeter finished executing, the returned function still remembers the greeting variable. That’s a closure!
Why Closures Matter
1. Data Privacy
Create private variables that can’t be accessed directly:
function createCounter() {
let count = 0; // Private variable
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined - can't access directly!
2. Function Factories
Create specialized functions:
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3. Event Handlers with Context
function createButtonHandler(buttonId) {
return function() {
console.log("Button " + buttonId + " was clicked!");
};
}
// Each button remembers its own ID
const button1Handler = createButtonHandler(1);
const button2Handler = createButtonHandler(2);
button1Handler(); // "Button 1 was clicked!"
button2Handler(); // "Button 2 was clicked!"
Real-World Examples
Example 1: User Session Management
function createUserSession(username) {
let loginTime = new Date();
let isActive = true;
return {
getUsername: function() {
return username;
},
getLoginTime: function() {
return loginTime;
},
logout: function() {
isActive = false;
console.log(username + " logged out");
},
isActive: function() {
return isActive;
}
};
}
const session = createUserSession("alice123");
console.log(session.getUsername()); // "alice123"
console.log(session.isActive()); // true
session.logout(); // "alice123 logged out"
console.log(session.isActive()); // false
Example 2: Once Function (run only once)
function once(fn) {
let hasRun = false;
let result;
return function(...args) {
if (!hasRun) {
result = fn(...args);
hasRun = true;
}
return result;
};
}
const initializeApp = once(() => {
console.log("App initialized!");
return "initialized";
});
initializeApp(); // "App initialized!"
initializeApp(); // Nothing logged - already ran
initializeApp(); // Still nothing
Example 3: Debounce Function
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
}, delay);
};
}
// Search function that waits for user to stop typing
const searchAPI = debounce((query) => {
console.log("Searching for:", query);
}, 500);
searchAPI("j"); // Waits...
searchAPI("ja"); // Waits...
searchAPI("jav"); // Waits...
searchAPI("java"); // After 500ms: "Searching for: java"
Common Pitfall: Closures in Loops
This is a classic mistake:
// Problem: All functions reference the same 'i'
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // What will this print?
}, i * 1000);
}
// Output: 4, 4, 4 (not 1, 2, 3!)
Why? By the time the functions run, the loop has finished and i is 4.
Solution 1: Use let instead of var
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// Output: 1, 2, 3 ✓
Solution 2: Create a closure with an IIFE
for (var i = 1; i <= 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index);
}, index * 1000);
})(i);
}
// Output: 1, 2, 3 ✓
Module Pattern
Closures enable the popular module pattern:
const ShoppingCart = (function() {
// Private variables and functions
let items = [];
function calculateTotal() {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Public API
return {
addItem: function(item) {
items.push(item);
},
removeItem: function(itemId) {
items = items.filter(item => item.id !== itemId);
},
getTotal: function() {
return calculateTotal();
},
getItemCount: function() {
return items.length;
}
};
})();
ShoppingCart.addItem({ id: 1, name: "Book", price: 10 });
ShoppingCart.addItem({ id: 2, name: "Pen", price: 2 });
console.log(ShoppingCart.getTotal()); // 12
console.log(ShoppingCart.getItemCount()); // 2
console.log(ShoppingCart.items); // undefined - private!
Memory Considerations
Closures keep variables in memory. Usually not a problem, but be aware:
function createHugeArray() {
const hugeArray = new Array(1000000).fill("data");
return function() {
// This closure keeps hugeArray in memory
return hugeArray[0];
};
}
// hugeArray stays in memory as long as the function exists
const getter = createHugeArray();
If you’re done with the closure, set it to null:
let getter = createHugeArray();
// ... use it ...
getter = null; // Now hugeArray can be garbage collected
Arrow Functions and Closures
Arrow functions work great with closures:
const createMultiplier = (factor) => {
return (number) => number * factor;
};
// Or even shorter
const createMultiplier = factor => number => number * factor;
const times5 = createMultiplier(5);
console.log(times5(3)); // 15
Key Takeaways
- Closures happen automatically when inner functions access outer variables
- They remember variables even after the outer function returns
- Use for: Private data, function factories, event handlers, modules
- Be careful: With loops using
var, and memory with large closures - They’re everywhere: In callbacks, event handlers, React hooks, and more
Practice
Try to understand what this code does:
function createGame() {
let score = 0;
let level = 1;
return {
earnPoints: (points) => {
score += points;
if (score >= level * 100) {
level++;
console.log("Level up! Now level " + level);
}
},
getStatus: () => ({
score: score,
level: level
})
};
}
const game = createGame();
game.earnPoints(50);
game.earnPoints(60); // "Level up! Now level 2"
console.log(game.getStatus()); // { score: 110, level: 2 }
Next Article: Spread and Rest Operators