Back to Blog
JavaScriptClosuresPatternsFunctions

JavaScript Closure Patterns

Master JavaScript closures. From data privacy to factory functions to advanced patterns.

B
Bootspring Team
Engineering
October 24, 2020
8 min read

Closures are fundamental to JavaScript. Here's how to use them effectively.

What Are Closures?#

1// A closure is a function that retains access to its outer scope 2function createCounter() { 3 let count = 0; // Private variable 4 5 return function() { 6 count++; // Access to outer variable 7 return count; 8 }; 9} 10 11const counter = createCounter(); 12console.log(counter()); // 1 13console.log(counter()); // 2 14console.log(counter()); // 3 15 16// Each call creates a new closure 17const counter2 = createCounter(); 18console.log(counter2()); // 1 (separate count) 19 20// The inner function "closes over" the outer variables 21function outer(x) { 22 return function inner(y) { 23 return x + y; // x is from outer scope 24 }; 25} 26 27const addFive = outer(5); 28console.log(addFive(3)); // 8 29console.log(addFive(10)); // 15

Data Privacy#

1// Module pattern using closures 2function createBankAccount(initialBalance) { 3 let balance = initialBalance; // Private 4 const transactions = []; // Private 5 6 function recordTransaction(type, amount) { 7 transactions.push({ 8 type, 9 amount, 10 date: new Date(), 11 balance, 12 }); 13 } 14 15 return { 16 deposit(amount) { 17 if (amount <= 0) throw new Error('Invalid amount'); 18 balance += amount; 19 recordTransaction('deposit', amount); 20 return balance; 21 }, 22 23 withdraw(amount) { 24 if (amount > balance) throw new Error('Insufficient funds'); 25 balance -= amount; 26 recordTransaction('withdrawal', amount); 27 return balance; 28 }, 29 30 getBalance() { 31 return balance; 32 }, 33 34 getTransactions() { 35 return [...transactions]; // Return copy 36 }, 37 }; 38} 39 40const account = createBankAccount(100); 41account.deposit(50); 42account.withdraw(30); 43console.log(account.getBalance()); // 120 44// account.balance = 1000000; // Won't affect actual balance

Factory Functions#

1// Create objects with private state 2function createPerson(name, age) { 3 // Private data 4 let _name = name; 5 let _age = age; 6 7 return { 8 getName() { 9 return _name; 10 }, 11 getAge() { 12 return _age; 13 }, 14 setName(newName) { 15 if (typeof newName !== 'string') { 16 throw new Error('Name must be string'); 17 } 18 _name = newName; 19 }, 20 haveBirthday() { 21 _age++; 22 return _age; 23 }, 24 }; 25} 26 27// Factory with configuration 28function createValidator(rules) { 29 return function validate(data) { 30 const errors = []; 31 32 for (const [field, rule] of Object.entries(rules)) { 33 if (rule.required && !data[field]) { 34 errors.push(`${field} is required`); 35 } 36 if (rule.minLength && data[field]?.length < rule.minLength) { 37 errors.push(`${field} must be at least ${rule.minLength} chars`); 38 } 39 if (rule.pattern && !rule.pattern.test(data[field])) { 40 errors.push(`${field} is invalid`); 41 } 42 } 43 44 return { valid: errors.length === 0, errors }; 45 }; 46} 47 48const validateUser = createValidator({ 49 email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }, 50 password: { required: true, minLength: 8 }, 51}); 52 53console.log(validateUser({ email: 'test@example.com', password: '12345678' }));

Memoization#

1// Cache function results using closure 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// Usage 19const expensiveCalculation = memoize((n) => { 20 console.log('Computing...'); 21 return n * n; 22}); 23 24console.log(expensiveCalculation(5)); // Computing... 25 25console.log(expensiveCalculation(5)); // 25 (cached) 26 27// Memoize with max size 28function memoizeWithLimit(fn, maxSize = 100) { 29 const cache = new Map(); 30 31 return function (...args) { 32 const key = JSON.stringify(args); 33 34 if (cache.has(key)) { 35 // Move to end (LRU) 36 const value = cache.get(key); 37 cache.delete(key); 38 cache.set(key, value); 39 return value; 40 } 41 42 const result = fn.apply(this, args); 43 44 if (cache.size >= maxSize) { 45 // Remove oldest 46 const firstKey = cache.keys().next().value; 47 cache.delete(firstKey); 48 } 49 50 cache.set(key, result); 51 return result; 52 }; 53}

Event Handlers#

1// Closure for event context 2function createButtonHandler(buttonId) { 3 let clickCount = 0; 4 5 return function handleClick(event) { 6 clickCount++; 7 console.log(`Button ${buttonId} clicked ${clickCount} times`); 8 }; 9} 10 11// Each button has its own count 12const button1Handler = createButtonHandler('save'); 13const button2Handler = createButtonHandler('cancel'); 14 15// Debounce using closures 16function debounce(fn, delay) { 17 let timeoutId; 18 19 return function (...args) { 20 clearTimeout(timeoutId); 21 22 timeoutId = setTimeout(() => { 23 fn.apply(this, args); 24 }, delay); 25 }; 26} 27 28const debouncedSearch = debounce((query) => { 29 console.log(`Searching for: ${query}`); 30}, 300); 31 32// Throttle using closures 33function throttle(fn, limit) { 34 let inThrottle = false; 35 36 return function (...args) { 37 if (!inThrottle) { 38 fn.apply(this, args); 39 inThrottle = true; 40 41 setTimeout(() => { 42 inThrottle = false; 43 }, limit); 44 } 45 }; 46} 47 48const throttledScroll = throttle(() => { 49 console.log('Scroll handled'); 50}, 100);

Partial Application#

1// Curry using closures 2function curry(fn) { 3 return function curried(...args) { 4 if (args.length >= fn.length) { 5 return fn.apply(this, args); 6 } 7 8 return function (...moreArgs) { 9 return curried.apply(this, args.concat(moreArgs)); 10 }; 11 }; 12} 13 14const add = (a, b, c) => a + b + c; 15const curriedAdd = curry(add); 16 17console.log(curriedAdd(1)(2)(3)); // 6 18console.log(curriedAdd(1, 2)(3)); // 6 19console.log(curriedAdd(1)(2, 3)); // 6 20 21// Partial application 22function partial(fn, ...presetArgs) { 23 return function (...laterArgs) { 24 return fn(...presetArgs, ...laterArgs); 25 }; 26} 27 28function greet(greeting, name, punctuation) { 29 return `${greeting}, ${name}${punctuation}`; 30} 31 32const sayHello = partial(greet, 'Hello'); 33const sayHelloExcitedly = partial(greet, 'Hello', 'World'); 34 35console.log(sayHello('World', '!')); // Hello, World! 36console.log(sayHelloExcitedly('!!!')); // Hello, World!!!

Iterators and Generators#

1// Iterator using closure 2function createRangeIterator(start, end, step = 1) { 3 let current = start; 4 5 return { 6 next() { 7 if (current <= end) { 8 const value = current; 9 current += step; 10 return { value, done: false }; 11 } 12 return { done: true }; 13 }, 14 }; 15} 16 17const range = createRangeIterator(1, 5); 18console.log(range.next()); // { value: 1, done: false } 19console.log(range.next()); // { value: 2, done: false } 20 21// State machine using closure 22function createStateMachine(config) { 23 let currentState = config.initial; 24 25 return { 26 getState() { 27 return currentState; 28 }, 29 30 transition(action) { 31 const transitions = config.states[currentState]?.on; 32 const nextState = transitions?.[action]; 33 34 if (nextState && config.states[nextState]) { 35 currentState = nextState; 36 return true; 37 } 38 39 return false; 40 }, 41 }; 42} 43 44const trafficLight = createStateMachine({ 45 initial: 'green', 46 states: { 47 green: { on: { TIMER: 'yellow' } }, 48 yellow: { on: { TIMER: 'red' } }, 49 red: { on: { TIMER: 'green' } }, 50 }, 51}); 52 53console.log(trafficLight.getState()); // green 54trafficLight.transition('TIMER'); 55console.log(trafficLight.getState()); // yellow

Singleton Pattern#

1// Singleton using closure 2const createDatabase = (function () { 3 let instance = null; 4 5 function Database(config) { 6 this.host = config.host; 7 this.port = config.port; 8 this.connected = false; 9 } 10 11 Database.prototype.connect = function () { 12 this.connected = true; 13 console.log(`Connected to ${this.host}:${this.port}`); 14 }; 15 16 return function (config) { 17 if (!instance) { 18 instance = new Database(config); 19 } 20 return instance; 21 }; 22})(); 23 24const db1 = createDatabase({ host: 'localhost', port: 5432 }); 25const db2 = createDatabase({ host: 'other', port: 3306 }); 26console.log(db1 === db2); // true (same instance) 27 28// Module singleton 29const logger = (function () { 30 const logs = []; 31 32 return { 33 log(message) { 34 const entry = { message, timestamp: Date.now() }; 35 logs.push(entry); 36 console.log(message); 37 }, 38 39 getLogs() { 40 return [...logs]; 41 }, 42 43 clear() { 44 logs.length = 0; 45 }, 46 }; 47})();

Loop Closures#

1// Classic closure problem 2for (var i = 0; i < 3; i++) { 3 setTimeout(() => console.log(i), 100); 4} 5// Logs: 3, 3, 3 (all reference same i) 6 7// Solution 1: let (block scope) 8for (let i = 0; i < 3; i++) { 9 setTimeout(() => console.log(i), 100); 10} 11// Logs: 0, 1, 2 12 13// Solution 2: IIFE 14for (var i = 0; i < 3; i++) { 15 (function (j) { 16 setTimeout(() => console.log(j), 100); 17 })(i); 18} 19// Logs: 0, 1, 2 20 21// Solution 3: Closure factory 22function createLogger(value) { 23 return function () { 24 console.log(value); 25 }; 26} 27 28for (var i = 0; i < 3; i++) { 29 setTimeout(createLogger(i), 100); 30} 31// Logs: 0, 1, 2

Once Function#

1// Execute function only once 2function once(fn) { 3 let called = false; 4 let result; 5 6 return function (...args) { 7 if (!called) { 8 called = true; 9 result = fn.apply(this, args); 10 } 11 return result; 12 }; 13} 14 15const initialize = once(() => { 16 console.log('Initializing...'); 17 return { initialized: true }; 18}); 19 20console.log(initialize()); // Initializing... { initialized: true } 21console.log(initialize()); // { initialized: true } (cached) 22console.log(initialize()); // { initialized: true } (cached) 23 24// After function - run after n calls 25function after(n, fn) { 26 let count = 0; 27 28 return function (...args) { 29 count++; 30 if (count >= n) { 31 return fn.apply(this, args); 32 } 33 }; 34} 35 36const logAfterThree = after(3, () => console.log('Called!')); 37logAfterThree(); // nothing 38logAfterThree(); // nothing 39logAfterThree(); // Called!

Best Practices#

Memory: ✓ Be aware of memory retention ✓ Null out large references when done ✓ Avoid creating closures in loops ✓ Use WeakMap for object keys Performance: ✓ Don't over-closurize ✓ Consider class for many instances ✓ Reuse closures when possible ✓ Profile memory usage Patterns: ✓ Use for data privacy ✓ Use for factory functions ✓ Use for callbacks with context ✓ Use for memoization Debugging: ✓ Name your functions ✓ Use descriptive variable names ✓ Add logging in development ✓ Understand scope chain

Conclusion#

Closures enable powerful patterns in JavaScript: data privacy, factories, memoization, and more. Understand how closures retain scope, be mindful of memory implications, and use them appropriately for cleaner, more maintainable code.

Share this article

Help spread the word about Bootspring