Closures are fundamental to JavaScript. Here's how they work and how to use them effectively.
What is a Closure?#
1// A closure is a function that remembers its lexical scope
2// even when executed outside that scope
3
4function outer() {
5 const message = 'Hello';
6
7 function inner() {
8 console.log(message); // Accesses outer's variable
9 }
10
11 return inner;
12}
13
14const greet = outer();
15greet(); // 'Hello' - still has access to message
16
17// The inner function "closes over" the outer scope
18// message is not garbage collected because inner still references itCreating Closures#
1// Function factories
2function createCounter() {
3 let count = 0;
4
5 return {
6 increment() {
7 count++;
8 return count;
9 },
10 decrement() {
11 count--;
12 return count;
13 },
14 getCount() {
15 return count;
16 },
17 };
18}
19
20const counter = createCounter();
21console.log(counter.increment()); // 1
22console.log(counter.increment()); // 2
23console.log(counter.decrement()); // 1
24console.log(counter.getCount()); // 1
25
26// Each call creates a new closure
27const counter2 = createCounter();
28console.log(counter2.getCount()); // 0 - independent
29
30// Closures with parameters
31function createMultiplier(multiplier) {
32 return function (number) {
33 return number * multiplier;
34 };
35}
36
37const double = createMultiplier(2);
38const triple = createMultiplier(3);
39
40console.log(double(5)); // 10
41console.log(triple(5)); // 15Private State#
1// Module pattern using closures
2function createBankAccount(initialBalance) {
3 let balance = initialBalance; // Private
4 const transactions = []; // Private
5
6 return {
7 deposit(amount) {
8 if (amount <= 0) throw new Error('Invalid amount');
9 balance += amount;
10 transactions.push({ type: 'deposit', amount, date: new Date() });
11 return balance;
12 },
13
14 withdraw(amount) {
15 if (amount <= 0) throw new Error('Invalid amount');
16 if (amount > balance) throw new Error('Insufficient funds');
17 balance -= amount;
18 transactions.push({ type: 'withdraw', amount, date: new Date() });
19 return balance;
20 },
21
22 getBalance() {
23 return balance;
24 },
25
26 getTransactions() {
27 return [...transactions]; // Return copy
28 },
29 };
30}
31
32const account = createBankAccount(100);
33account.deposit(50); // 150
34account.withdraw(30); // 120
35account.balance; // undefined - private!
36account.getBalance(); // 120Closures in Loops#
1// Classic closure gotcha
2// Wrong - all callbacks share same i
3for (var i = 0; i < 3; i++) {
4 setTimeout(function () {
5 console.log(i); // 3, 3, 3
6 }, 100);
7}
8
9// Solution 1: let (block scoping)
10for (let i = 0; i < 3; i++) {
11 setTimeout(function () {
12 console.log(i); // 0, 1, 2
13 }, 100);
14}
15
16// Solution 2: IIFE
17for (var i = 0; i < 3; i++) {
18 (function (j) {
19 setTimeout(function () {
20 console.log(j); // 0, 1, 2
21 }, 100);
22 })(i);
23}
24
25// Solution 3: forEach with callback
26[0, 1, 2].forEach(function (i) {
27 setTimeout(function () {
28 console.log(i); // 0, 1, 2
29 }, 100);
30});Event Handlers#
1// Closures in event handlers
2function setupButton(buttonId, message) {
3 const button = document.getElementById(buttonId);
4
5 button.addEventListener('click', function () {
6 alert(message); // Closure captures message
7 });
8}
9
10setupButton('btn1', 'Hello!');
11setupButton('btn2', 'Goodbye!');
12
13// With cleanup
14function setupButtonWithCleanup(buttonId, message) {
15 const button = document.getElementById(buttonId);
16
17 function handler() {
18 alert(message);
19 }
20
21 button.addEventListener('click', handler);
22
23 // Return cleanup function
24 return function cleanup() {
25 button.removeEventListener('click', handler);
26 };
27}
28
29const cleanup = setupButtonWithCleanup('btn1', 'Hello!');
30// Later...
31cleanup(); // Removes event listenerMemoization#
1// Caching with closures
2function memoize(fn) {
3 const cache = new Map();
4
5 return function (...args) {
6 const key = JSON.stringify(args);
7
8 if (cache.has(key)) {
9 return cache.get(key);
10 }
11
12 const result = fn.apply(this, args);
13 cache.set(key, result);
14 return result;
15 };
16}
17
18// Expensive function
19function fibonacci(n) {
20 if (n <= 1) return n;
21 return fibonacci(n - 1) + fibonacci(n - 2);
22}
23
24const memoizedFib = memoize(function fib(n) {
25 if (n <= 1) return n;
26 return memoizedFib(n - 1) + memoizedFib(n - 2);
27});
28
29console.log(memoizedFib(40)); // Fast due to caching
30
31// Memoize with max cache size
32function memoizeWithLimit(fn, maxSize = 100) {
33 const cache = new Map();
34
35 return function (...args) {
36 const key = JSON.stringify(args);
37
38 if (cache.has(key)) {
39 // Move to end (LRU)
40 const value = cache.get(key);
41 cache.delete(key);
42 cache.set(key, value);
43 return value;
44 }
45
46 const result = fn.apply(this, args);
47
48 if (cache.size >= maxSize) {
49 // Remove oldest entry
50 const firstKey = cache.keys().next().value;
51 cache.delete(firstKey);
52 }
53
54 cache.set(key, result);
55 return result;
56 };
57}Currying and Partial Application#
1// Currying with closures
2function curry(fn) {
3 return function curried(...args) {
4 if (args.length >= fn.length) {
5 return fn.apply(this, args);
6 }
7 return function (...moreArgs) {
8 return curried.apply(this, [...args, ...moreArgs]);
9 };
10 };
11}
12
13function add(a, b, c) {
14 return a + b + c;
15}
16
17const curriedAdd = curry(add);
18console.log(curriedAdd(1)(2)(3)); // 6
19console.log(curriedAdd(1, 2)(3)); // 6
20console.log(curriedAdd(1)(2, 3)); // 6
21
22// Partial application
23function partial(fn, ...presetArgs) {
24 return function (...laterArgs) {
25 return fn(...presetArgs, ...laterArgs);
26 };
27}
28
29function greet(greeting, name) {
30 return `${greeting}, ${name}!`;
31}
32
33const sayHello = partial(greet, 'Hello');
34console.log(sayHello('World')); // "Hello, World!"Closures in React#
1// useState closure
2function Counter() {
3 const [count, setCount] = useState(0);
4
5 // Each render creates a new closure
6 const handleClick = () => {
7 setCount(count + 1); // Captures count from this render
8 };
9
10 // Problem: stale closure
11 useEffect(() => {
12 const interval = setInterval(() => {
13 console.log(count); // Always logs initial count!
14 }, 1000);
15
16 return () => clearInterval(interval);
17 }, []); // Empty deps - stale closure
18
19 // Solution: functional update
20 useEffect(() => {
21 const interval = setInterval(() => {
22 setCount((c) => c + 1); // No closure issue
23 }, 1000);
24
25 return () => clearInterval(interval);
26 }, []);
27
28 return <button onClick={handleClick}>{count}</button>;
29}
30
31// useCallback captures values
32function SearchComponent() {
33 const [query, setQuery] = useState('');
34
35 // Stale closure if query not in deps
36 const search = useCallback(() => {
37 fetchResults(query);
38 }, [query]); // Must include query
39
40 return <SearchButton onClick={search} />;
41}Memory Considerations#
1// Closures keep references alive
2function createLargeArray() {
3 const largeArray = new Array(1000000).fill('data');
4
5 return function () {
6 return largeArray.length; // largeArray stays in memory
7 };
8}
9
10const getLength = createLargeArray();
11// largeArray is not garbage collected
12
13// Solution: Only capture what you need
14function createLargeArrayFixed() {
15 const largeArray = new Array(1000000).fill('data');
16 const length = largeArray.length;
17
18 return function () {
19 return length; // Only captures the number
20 };
21 // largeArray can be garbage collected
22}
23
24// Accidental closures
25function setupHandler() {
26 const data = fetchSomeData(); // Large object
27
28 element.onclick = function () {
29 // Captures entire scope, including data
30 console.log('clicked');
31 };
32}
33
34// Better: Don't create unnecessary closures
35function setupHandlerFixed() {
36 const data = fetchSomeData();
37 processData(data);
38 // data goes out of scope
39
40 element.onclick = handleClick; // No closure
41}
42
43function handleClick() {
44 console.log('clicked');
45}Best Practices#
Understanding:
✓ Know what variables are captured
✓ Understand scope chain
✓ Watch for stale closures
✓ Consider memory implications
Patterns:
✓ Use for private state
✓ Use for factory functions
✓ Use for memoization
✓ Use for event handlers
Avoid:
✗ Capturing large objects unnecessarily
✗ Creating closures in loops with var
✗ Forgetting useCallback dependencies
✗ Overcomplicating simple code
Conclusion#
Closures enable powerful patterns like private state, factories, and memoization. Understanding how closures capture their lexical scope helps avoid common pitfalls like stale closures and memory leaks. Use them intentionally for cleaner, more functional code.