In short
A JavaScript closure is a function that remembers the variables from where it was created. Learn closures, private state, function factories, higher-order functions and debounce. What is a closure in JavaScript?
A JavaScript closure is a function that remembers the variables from where it was created. Learn closures, private state, function factories, higher-order functions and debounce.
What is a closure in JavaScript?
When you create a function inside another function, the inner function can use the outer function's variables. The surprise is that it keeps access to them even after the outer function has finished. This is called a closure.
Think of a backpack. When the inner function is created, it packs the variables it can see into a backpack and carries them wherever it goes.
function createCounter() {
let count = 0; // lives inside createCounter
return function () {
count++; // still has access to count
return count;
};
}
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA()); // Output: 1
console.log(counterA()); // Output: 2
console.log(counterA()); // Output: 3
console.log(counterB()); // Output: 1
createCounter has finished running, but count is not deleted, because the returned function still uses it. Each call to createCounter makes a new count, so counterA and counterB each have their own backpack.
How to use closures for private state
Closures let you hide data so that only certain functions can change it. This works like the #private fields in classes, but with plain functions.
function createWallet(startAmount) {
let balance = startAmount; // private: no one outside can reach it
return {
spend(amount) {
if (amount > balance) return "Not enough money";
balance -= amount;
return `Spent $${amount}. Left: $${balance}`;
},
getBalance() {
return balance;
},
};
}
const wallet = createWallet(50);
console.log(wallet.spend(20)); // Output: Spent $20. Left: $30
console.log(wallet.spend(100)); // Output: Not enough money
console.log(wallet.getBalance()); // Output: 30
console.log(wallet.balance); // Output: undefined (it is hidden)
The only way to change balance is through spend, which checks the rules. Nobody can set wallet.balance = 1000000.
What are function factories?
A function factory is a function that builds and returns other functions, each set up a little differently. It is a factory line: same machine, different settings.
function createGreeter(greeting) {
return (name) => `${greeting}, ${name}!`;
}
const sayHello = createGreeter("Hello");
const sayHola = createGreeter("Hola");
console.log(sayHello("Maya")); // Output: Hello, Maya!
console.log(sayHola("Leo")); // Output: Hola, Leo!
const createMultiplier = (factor) => (n) => n * factor;
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5), triple(5)); // Output: 10 15
Each returned function remembers its own greeting or factor through a closure.
What is a higher-order function?
A higher-order function is a function that takes a function as an argument, returns a function, or both. You already use them: map, filter, forEach and addEventListener all take functions. createGreeter above returns one.
Here is a simple higher-order function you can write yourself. It takes any function and returns a new version that logs each call:
function withLogging(fn) {
return (...args) => {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log("Result:", result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// Output:
// Calling add with [2, 3]
// Result: 5
withLogging does not change add; it wraps it. This "wrap a function to add behavior" idea is used everywhere in real projects.
Currying is a related idea: turning a function that takes several arguments into a chain of functions that each take one. createMultiplier above is curried: createMultiplier(2)(5) gives 10. It is handy when you want to set some arguments now and the rest later. You do not need to use it often, but you will recognize it in other people's code.
Debounce: a real-world closure example
Imagine a search box that calls an API on every key press. Typing "javascript" would send 10 requests. Debounce waits until the user stops typing for a short time, then runs the function once. It uses a closure to remember the timer between calls.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Debounce search</title>
</head>
<body>
<input id="search" type="search" placeholder="Search products...">
<p id="result">Start typing...</p>
<script>
function debounce(fn, delay) {
let timerId; // remembered between calls thanks to the closure
return (...args) => {
clearTimeout(timerId); // cancel the previous timer
timerId = setTimeout(() => fn(...args), delay); // start a new one
};
}
const result = document.querySelector("#result");
function search(text) {
console.log("Searching for:", text);
result.textContent = `Searching for "${text}"...`;
}
const debouncedSearch = debounce(search, 400);
document.querySelector("#search").addEventListener("input", (event) => {
debouncedSearch(event.target.value);
});
</script>
</body>
</html>
Type "laptop" quickly. Instead of six log lines, you see only one: Searching for: laptop, about 400 ms after your last key press. Every key press clears the old timer and starts a new one. The timerId variable survives between calls because the returned arrow function closes over it.
Note: Closures keep their variables alive as long as the function exists. Usually this is fine. But if a closure holds something big (like a huge array or many DOM elements) and you keep the function forever, for example in an event listener you never remove, that memory cannot be freed. Remove listeners you no longer need, and don't store more than the closure really uses.
Common mistake: Using
varin a loop with callbacks.for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));prints3 3 3, because all callbacks share onei. Withlet, each loop round gets its owni, so it prints0 1 2.
Practice exercise
Write a function once(fn) that returns a new function. The new function runs fn only the first time it is called and returns the same result on later calls. Test it with a "Pay now" button so that clicking many times only "pays" once.
Hint: keep two variables in the closure: let called = false; and let result;.
Key takeaways
- A closure is a function that remembers the variables from where it was created.
- Closures give you private state and power function factories.
- A higher-order function takes or returns a function, like
mapordebounce. - Debounce uses a closure to keep one timer and run a function only after a pause.
- Closures keep their variables in memory, so remove unused listeners and avoid holding big data.