Closures are one of the most powerful features in JavaScript. This guide explains closures, how they work, and practical applications.
What is a Closure? #
A closure is a function that has access to variables in its outer (enclosing) scope, even after the outer function has returned.
function outer() {
const message = "Hello";
function inner() {
console.log(message); // Can access outer variable
}
return inner;
}
const myFunc = outer();
myFunc(); // "Hello" - closure remembers 'message'
How Closures Work #
function makeCounter() {
let count = 0; // Private variable
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// Each call remembers previous count
Lexical Scoping #
Closures work because of lexical scoping - functions have access to variables in their outer scope.
function outer() {
const x = 10;
function middle() {
const y = 20;
function inner() {
console.log(x + y); // Can access both x and y
}
return inner;
}
return middle();
}
const func = outer();
func(); // 30
Practical Examples #
Private Variables #
function createPerson(name) {
let age = 0; // Private
return {
getName() {
return name;
},
getAge() {
return age;
},
growOlder() {
age++;
}
};
}
const person = createPerson("Alice");
console.log(person.getName()); // "Alice"
person.growOlder();
console.log(person.getAge()); // 1
// console.log(person.age); // undefined - private!
Counter with Multiple Operations #
function createCounter(initial = 0) {
let count = initial;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
reset() {
count = initial;
return count;
},
getValue() {
return count;
}
};
}
const counter = createCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.decrement(); // 11
counter.reset(); // 10
Function Factory #
function multiply(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Event Handlers #
function createButton(label) {
let clickCount = 0;
return {
getLabel() {
return label;
},
click() {
clickCount++;
console.log(`${label} clicked ${clickCount} times`);
}
};
}
const submitButton = createButton("Submit");
submitButton.click(); // "Submit clicked 1 times"
submitButton.click(); // "Submit clicked 2 times"
Module Pattern #
const calculator = (function() {
let result = 0; // Private
return {
add(n) {
result += n;
return this;
},
subtract(n) {
result -= n;
return this;
},
multiply(n) {
result *= n;
return this;
},
getResult() {
return result;
},
clear() {
result = 0;
return this;
}
};
})();
calculator
.add(5)
.multiply(2)
.subtract(3);
console.log(calculator.getResult()); // 7
Memoization #
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("From cache");
return cache[key];
}
console.log("Calculating");
const result = fn(...args);
cache[key] = result;
return result;
};
}
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // Calculates
console.log(fibonacci(10)); // From cache
Partial Application #
function partial(fn, ...args) {
return function(...moreArgs) {
return fn(...args, ...moreArgs);
};
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, "Hello");
console.log(sayHello("Alice")); // "Hello, Alice!"
const sayGoodbye = partial(greet, "Goodbye");
console.log(sayGoodbye("Bob")); // "Goodbye, Bob!"
Curry Function #
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return function(...moreArgs) {
return curried(...args, ...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
Common Pitfalls #
Loop and Closures #
Problem:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3
}, 1000);
}Solution 1: IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2
}, 1000);
})(i);
}Solution 2: let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 1000);
}Memory Leaks #
function attachHandler() {
const element = document.getElementById('button');
const data = new Array(1000000); // Large data
// Closure keeps reference to data
element.addEventListener('click', function() {
console.log(data.length);
});
}
// Better: only keep what you need
function attachHandler() {
const element = document.getElementById('button');
const dataLength = new Array(1000000).length;
element.addEventListener('click', function() {
console.log(dataLength);
});
}Real-World Applications #
Debounce Function #
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
}, delay);
};
}
const search = debounce(function(query) {
console.log("Searching for:", query);
}, 500);
// Only last call after 500ms executes
search("a");
search("ab");
search("abc"); // Only this executes
Throttle Function #
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn(...args);
}
};
}
const handleScroll = throttle(function() {
console.log("Scrolled");
}, 1000);
window.addEventListener('scroll', handleScroll);Once Function #
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(function() {
console.log("Initializing");
return "Initialized";
});
initialize(); // "Initializing", returns "Initialized"
initialize(); // Returns "Initialized" (doesn't log)
Rate Limiter #
function createRateLimiter(maxCalls, timeWindow) {
const calls = [];
return function(fn) {
const now = Date.now();
// Remove old calls
while (calls.length > 0 && calls[0] < now - timeWindow) {
calls.shift();
}
if (calls.length < maxCalls) {
calls.push(now);
return fn();
} else {
throw new Error("Rate limit exceeded");
}
};
}
const limiter = createRateLimiter(3, 1000); // 3 calls per second
limiter(() => console.log("Call 1"));
limiter(() => console.log("Call 2"));
limiter(() => console.log("Call 3"));
// limiter(() => console.log("Call 4")); // Error
Closure Interview Questions #
Question 1: What will this log? #
function test() {
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
}
test();
// Answer: 3, 3, 3 (all reference same 'i')
Question 2: Fix the code #
function createFunctions() {
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
return i;
});
}
return funcs;
}
// Fix:
function createFunctions() {
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function() {
return i;
});
}
return funcs;
}Best Practices #
- Understand scope - Know what variables are captured
- Avoid memory leaks - Don’t capture unnecessary data
- Use closures wisely - Don’t overuse for simple cases
- Prefer const/let over var to avoid issues
- Document closure behavior - Help other developers
- Test edge cases - Especially with async code
Closures are fundamental to JavaScript. Understanding them deeply will make you a better developer and help you write more elegant, functional code.