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)); // 15Data 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 balanceFactory 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()); // yellowSingleton 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, 2Once 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.