Back to Blog
JavaScriptClosuresFunctionsScope

JavaScript Closures Explained

Master JavaScript closures from basic concepts to practical patterns and common pitfalls.

B
Bootspring Team
Engineering
March 26, 2020
6 min read

A closure is a function that remembers its lexical scope even when executed outside that scope. Here's how they work.

Basic Closure#

1function outer() { 2 const message = 'Hello'; // Variable in outer scope 3 4 function inner() { 5 console.log(message); // Accesses outer variable 6 } 7 8 return inner; 9} 10 11const greet = outer(); 12greet(); // 'Hello' - closure remembers message 13 14// The inner function "closes over" the message variable 15// Even after outer() has returned, inner still has access

Creating Private State#

1// Counter with private state 2function createCounter() { 3 let count = 0; // Private variable 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// console.log(counter.count); // undefined - truly private 26 27// Multiple counters are independent 28const counter2 = createCounter(); 29console.log(counter2.getCount()); // 0 - separate closure

Function Factories#

1// Create specialized functions 2function multiply(a) { 3 return function(b) { 4 return a * b; 5 }; 6} 7 8const double = multiply(2); 9const triple = multiply(3); 10 11console.log(double(5)); // 10 12console.log(triple(5)); // 15 13 14// Tax calculator factory 15function createTaxCalculator(rate) { 16 return function(amount) { 17 return amount * (1 + rate); 18 }; 19} 20 21const calculateWithVAT = createTaxCalculator(0.20); 22const calculateWithSalesTax = createTaxCalculator(0.08); 23 24console.log(calculateWithVAT(100)); // 120 25console.log(calculateWithSalesTax(100)); // 108 26 27// Logger factory 28function createLogger(prefix) { 29 return function(message) { 30 console.log(`[${prefix}] ${message}`); 31 }; 32} 33 34const infoLog = createLogger('INFO'); 35const errorLog = createLogger('ERROR'); 36 37infoLog('Application started'); // [INFO] Application started 38errorLog('Connection failed'); // [ERROR] Connection failed

Event Handlers#

1// Closure in event handlers 2function setupButtons() { 3 const buttons = document.querySelectorAll('.btn'); 4 5 buttons.forEach((button, index) => { 6 button.addEventListener('click', () => { 7 // Closure captures 'index' for each button 8 console.log(`Button ${index} clicked`); 9 }); 10 }); 11} 12 13// Without closure (common mistake) 14function setupButtonsBad() { 15 const buttons = document.querySelectorAll('.btn'); 16 17 for (var i = 0; i < buttons.length; i++) { 18 buttons[i].addEventListener('click', function() { 19 // All buttons log same value (buttons.length) 20 console.log(`Button ${i} clicked`); 21 }); 22 } 23} 24 25// Fix with let (block scope) 26function setupButtonsFixed() { 27 const buttons = document.querySelectorAll('.btn'); 28 29 for (let i = 0; i < buttons.length; i++) { 30 buttons[i].addEventListener('click', function() { 31 console.log(`Button ${i} clicked`); 32 }); 33 } 34} 35 36// Fix with IIFE 37function setupButtonsIIFE() { 38 const buttons = document.querySelectorAll('.btn'); 39 40 for (var i = 0; i < buttons.length; i++) { 41 (function(index) { 42 buttons[index].addEventListener('click', function() { 43 console.log(`Button ${index} clicked`); 44 }); 45 })(i); 46 } 47}

Module Pattern#

1// Classic module pattern using closure 2const userModule = (function() { 3 // Private variables 4 let users = []; 5 let nextId = 1; 6 7 // Private functions 8 function generateId() { 9 return nextId++; 10 } 11 12 // Public API 13 return { 14 addUser(name) { 15 const user = { id: generateId(), name }; 16 users.push(user); 17 return user; 18 }, 19 20 getUser(id) { 21 return users.find(u => u.id === id); 22 }, 23 24 getAllUsers() { 25 return [...users]; // Return copy to protect internal array 26 }, 27 28 removeUser(id) { 29 users = users.filter(u => u.id !== id); 30 } 31 }; 32})(); 33 34userModule.addUser('Alice'); 35userModule.addUser('Bob'); 36console.log(userModule.getAllUsers()); 37// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

Memoization#

1// Cache function results using closure 2function memoize(fn) { 3 const cache = {}; 4 5 return function(...args) { 6 const key = JSON.stringify(args); 7 8 if (key in cache) { 9 console.log('Cache hit'); 10 return cache[key]; 11 } 12 13 console.log('Cache miss'); 14 const result = fn.apply(this, args); 15 cache[key] = result; 16 return result; 17 }; 18} 19 20// Expensive calculation 21function fibonacci(n) { 22 if (n <= 1) return n; 23 return fibonacci(n - 1) + fibonacci(n - 2); 24} 25 26const memoizedFib = memoize(function fib(n) { 27 if (n <= 1) return n; 28 return memoizedFib(n - 1) + memoizedFib(n - 2); 29}); 30 31console.log(memoizedFib(40)); // Fast with memoization

Partial Application#

1// Fix some arguments using closure 2function partial(fn, ...fixedArgs) { 3 return function(...remainingArgs) { 4 return fn(...fixedArgs, ...remainingArgs); 5 }; 6} 7 8function greet(greeting, name, punctuation) { 9 return `${greeting}, ${name}${punctuation}`; 10} 11 12const sayHello = partial(greet, 'Hello'); 13const sayHelloExcited = partial(greet, 'Hello', 'World'); 14 15console.log(sayHello('Alice', '!')); // 'Hello, Alice!' 16console.log(sayHelloExcited('!')); // 'Hello, World!' 17 18// Practical: API request helper 19function request(baseUrl, method, endpoint, data) { 20 return fetch(`${baseUrl}${endpoint}`, { 21 method, 22 body: JSON.stringify(data) 23 }); 24} 25 26const apiRequest = partial(request, 'https://api.example.com'); 27const apiPost = partial(apiRequest, 'POST'); 28 29apiPost('/users', { name: 'Alice' });

Debounce and Throttle#

1// Debounce using closure 2function debounce(fn, delay) { 3 let timeoutId; 4 5 return function(...args) { 6 clearTimeout(timeoutId); 7 timeoutId = setTimeout(() => { 8 fn.apply(this, args); 9 }, delay); 10 }; 11} 12 13const debouncedSearch = debounce((query) => { 14 console.log('Searching:', query); 15}, 300); 16 17// Throttle using closure 18function throttle(fn, interval) { 19 let lastTime = 0; 20 let timeoutId; 21 22 return function(...args) { 23 const now = Date.now(); 24 25 if (now - lastTime >= interval) { 26 lastTime = now; 27 fn.apply(this, args); 28 } 29 }; 30} 31 32const throttledScroll = throttle(() => { 33 console.log('Scroll handler'); 34}, 100);

Common Pitfalls#

1// Loop variable capture (var) 2for (var i = 0; i < 3; i++) { 3 setTimeout(() => console.log(i), 100); 4} 5// Logs: 3, 3, 3 6 7// Fix with let 8for (let i = 0; i < 3; i++) { 9 setTimeout(() => console.log(i), 100); 10} 11// Logs: 0, 1, 2 12 13// Memory leaks - closure holds reference 14function createHandler() { 15 const largeData = new Array(1000000).fill('x'); 16 17 return function() { 18 // largeData stays in memory as long as handler exists 19 console.log(largeData.length); 20 }; 21} 22 23// Fix: only capture what you need 24function createHandlerFixed() { 25 const largeData = new Array(1000000).fill('x'); 26 const length = largeData.length; // Capture only length 27 28 return function() { 29 console.log(length); 30 }; 31} 32 33// Accidental global 34function badClosure() { 35 // Missing 'let' or 'const' creates global 36 secret = 'exposed'; 37 38 return function() { 39 console.log(secret); 40 }; 41}

Advanced Patterns#

1// Currying 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 13const add = curry((a, b, c) => a + b + c); 14console.log(add(1)(2)(3)); // 6 15console.log(add(1, 2)(3)); // 6 16console.log(add(1)(2, 3)); // 6 17 18// Once - function that runs only once 19function once(fn) { 20 let called = false; 21 let result; 22 23 return function(...args) { 24 if (!called) { 25 called = true; 26 result = fn.apply(this, args); 27 } 28 return result; 29 }; 30} 31 32const initialize = once(() => { 33 console.log('Initializing...'); 34 return 'initialized'; 35}); 36 37initialize(); // 'Initializing...' 38initialize(); // (nothing logged, returns 'initialized')

Best Practices#

Use Closures For: ✓ Private state and encapsulation ✓ Function factories ✓ Partial application ✓ Memoization and caching Memory Considerations: ✓ Capture only needed variables ✓ Nullify references when done ✓ Watch for circular references ✓ Profile memory usage Avoid: ✗ Capturing loop variables with var ✗ Excessive closure depth ✗ Capturing large objects unnecessarily ✗ Creating closures in tight loops

Conclusion#

Closures are fundamental to JavaScript, enabling private state, function factories, and powerful patterns like memoization and partial application. Understand lexical scope and be mindful of memory implications. Use let/const in loops to avoid common variable capture issues.

Share this article

Help spread the word about Bootspring