If you've ever struggled with javascript closures, you're not alone. In this hands-on tutorial, I'll walk you through everything from the basics to real-world applications — with code you can actually use in your projects.
What Are Closures?
A closure is a function that remembers the variables from its outer scope, even after that outer function has finished executing. It's one of JavaScript's most powerful features.
Here's the simplest possible closure:
function createGreeter(name) {
// 'name' is captured by the inner function
return function() {
console.log(`Hello, ${name}!`);
};
}
const greetAlice = createGreeter('Alice');
const greetBob = createGreeter('Bob');
greetAlice(); // "Hello, Alice!"
greetBob(); // "Hello, Bob!"
// Each function "remembers" its own 'name' value
The inner function closes over the variable name. Even though createGreeter has already returned, the inner function still has access to name.
How Closures Work Under the Hood
When JavaScript creates a function, it also creates a lexical environment — a record of all variables in scope at that point. The closure holds a reference to this environment.
// Step-by-step execution:
function outer() {
let count = 0; // This lives in outer's lexical environment
function inner() {
count++; // inner has a reference to outer's environment
return count;
}
return inner;
}
const counter = outer();
// outer() is done, but its lexical environment survives
// because 'inner' still references it
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 'count' persists between calls!
Practical Closure Patterns
Here are the most useful closure patterns you'll encounter in real projects:
Pattern 1: Private Variables (Module Pattern)
const bankAccount = (function() {
let balance = 0; // Private — can't be accessed directly
return {
deposit(amount) {
if (amount <= 0) throw new Error('Amount must be positive');
balance += amount;
return `Deposited $${amount}. Balance: $${balance}`;
},
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return `Withdrew $${amount}. Balance: $${balance}`;
},
getBalance() {
return `$${balance}`;
}
};
})();
console.log(bankAccount.deposit(100)); // "Deposited $100. Balance: $100"
console.log(bankAccount.withdraw(30)); // "Withdrew $30. Balance: $70"
console.log(bankAccount.getBalance()); // "$70"
// bankAccount.balance → undefined (it's private!)
Pattern 2: Function Factory
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenTimes = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50
// Useful for creating configurable functions
const addTax = createMultiplier(1.08); // 8% tax
console.log(addTax(100)); // 108
Pattern 3: Event Handler with State
function createClickCounter(buttonId) {
let clicks = 0;
const button = document.getElementById(buttonId);
button.addEventListener('click', function() {
clicks++;
button.textContent = `Clicked ${clicks} times`;
if (clicks >= 10) {
button.disabled = true;
button.textContent = 'Max clicks reached!';
}
});
}
// Each button gets its own independent counter
createClickCounter('btn-1');
createClickCounter('btn-2');
Common Use Cases
Closures appear everywhere in JavaScript. Here are real-world scenarios:
Debounce Function
function debounce(func, delay) {
let timeoutId; // Closed over by the returned function
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: Only trigger search after user stops typing for 300ms
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function(e) {
console.log('Searching for:', e.target.value);
// Make API call here
}, 300));
Memoization / Caching
function memoize(fn) {
const cache = {}; // Closure keeps the cache alive
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
console.log('Cache hit!');
return cache[key];
}
console.log('Computing...');
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveCalc = memoize(function(n) {
// Simulate expensive operation
let result = 0;
for (let i = 0; i < n * 1000000; i++) result += i;
return result;
});
console.log(expensiveCalc(100)); // "Computing..." → result
console.log(expensiveCalc(100)); // "Cache hit!" → same result instantly
Closures and Memory
Closures keep variables alive in memory. This is usually fine, but can cause issues in loops:
The Classic Loop Problem
// BUG: All callbacks print 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // Always 5! var is function-scoped
}, i * 100);
}
// FIX 1: Use let (block-scoped)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2, 3, 4 ✓
}, i * 100);
}
// FIX 2: Use IIFE to create new scope
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2, 3, 4 ✓
}, j * 100);
})(i);
}
null when you're done.Practice Exercises
Exercise 1: Create a Counter
Build a makeCounter() that returns an object with increment(), decrement(), and getValue():
function makeCounter(initial = 0) {
let count = initial;
return {
increment() { return ++count; },
decrement() { return --count; },
getValue() { return count; },
reset() { count = initial; return count; }
};
}
const c = makeCounter(10);
console.log(c.increment()); // 11
console.log(c.increment()); // 12
console.log(c.decrement()); // 11
console.log(c.reset()); // 10
Exercise 2: Rate Limiter
function createRateLimiter(maxCalls, perSeconds) {
const timestamps = [];
return function(...args) {
const now = Date.now();
// Remove timestamps older than the window
while (timestamps.length && timestamps[0] < now - perSeconds * 1000) {
timestamps.shift();
}
if (timestamps.length >= maxCalls) {
console.log('Rate limit exceeded! Try again later.');
return null;
}
timestamps.push(now);
console.log(`Call allowed (${timestamps.length}/${maxCalls})`);
return true;
};
}
const limiter = createRateLimiter(3, 10); // 3 calls per 10 seconds
limiter(); // "Call allowed (1/3)"
limiter(); // "Call allowed (2/3)"
limiter(); // "Call allowed (3/3)"
limiter(); // "Rate limit exceeded!"
Full-Stack Developer & Technical Writer at DRIXO
Full-stack developer with 5+ years of experience in Python and JavaScript. I love breaking down complex concepts into simple, practical tutorials. When I'm not coding, you'll find me contributing to open-source projects.
Comments