Back to Blog
JavaScriptClosuresFundamentalsFunctions

JavaScript Closures Explained

Understand JavaScript closures. From basic concepts to practical applications to common pitfalls.

B
Bootspring Team
Engineering
November 8, 2021
6 min read

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 it

Creating 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)); // 15

Private 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(); // 120

Closures 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 listener

Memoization#

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.

Share this article

Help spread the word about Bootspring