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 accessCreating 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 closureFunction 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 failedEvent 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 memoizationPartial 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.