Closure in JavaScript

Ashish Kumar
14 min readJul 24, 2023

JavaScript is a function that remembers and can access variables from its parent function, even after the parent function has finished executing. It allows for data encapsulation and maintaining state across function calls. Closures are created when a function is defined inside another function or returned from it.

Examples of using closures in JavaScript:

  1. Private Variables and Encapsulation:
    Closures can be used to create private variables and achieve encapsulation by controlling the access to certain data.
function createCounter() {
let count = 0;

return {
increment: function() {
count++;
},
decrement: function() {
count--;
},
getCount: function() {
return count;
}
};
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // Output: 2
counter.decrement();
console.log(counter.getCount()); // Output: 1

In this example, the createCounter function returns an object with three methods (increment, decrement, and getCount). The count variable is private to the closure and cannot be accessed directly from outside the createCounter function. The returned methods have access to the count variable, enabling you to increment, decrement, and retrieve the count value while maintaining its privacy.

2. Function Factories:
Closures can be used to create function factories, where you generate functions with customized behavior based on a common pattern.

function multiplyBy(factor) {
return function(num) {
return num * factor;
};
}

const multiplyByTwo = multiplyBy(2);
const multiplyByThree = multiplyBy(3);

console.log(multiplyByTwo(5)); // Output: 10
console.log(multiplyByThree(5)); // Output: 15

In this example, the multiplyBy function returns a new function that multiplies its input by the factor provided. The returned functions (multiplyByTwo and multiplyByThree) are closures, and each maintains its specific factor value. You can reuse these functions to perform different multiplications based on the initial factor.

3. Memoization (Caching):
Closures can be used for memoization, a technique to cache expensive function calls and avoid redundant calculations.

function fibonacci() {
const cache = {};

return function fib(n) {
if (n in cache) {
return cache[n];
}

if (n <= 2) {
return 1;
}

const result = fib(n - 1) + fib(n - 2);
cache[n] = result;
return result;
};
}

const memoizedFibonacci = fibonacci();

console.log(memoizedFibonacci(10)); // Output: 55 (calculated and cached)
console.log(memoizedFibonacci(15)); // Output: 610 (calculated and cached)
console.log(memoizedFibonacci(10)); // Output: 55 (retrieved from cache, not recalculated)

In this example, the fibonacci function returns a closure function fib, which calculates Fibonacci numbers using memoization. The cache object stores previously calculated Fibonacci values, so the function avoids redundant calculations for the same input.

These examples demonstrate some practical use cases of closures in JavaScript, such as creating private variables, function factories, and memoization. Closures are a powerful concept that allows you to control the visibility and lifetime of variables within your functions, leading to more flexible and maintainable code.

4. Event Handlers with Private Data:

Closures can be used to create event handlers that have access to private data specific to each instance of the event handler.

<button id="btn1">Button 1</button>
<button id="btn2">Button 2</button>
function createClickHandler(buttonName) {
let clickCount = 0;

return function() {
clickCount++;
console.log(`${buttonName} clicked ${clickCount} times.`);
};
}

const btn1 = document.getElementById('btn1');
const btn2 = document.getElementById('btn2');

btn1.addEventListener('click', createClickHandler('Button 1'));
btn2.addEventListener('click', createClickHandler('Button 2'));

In this example, the createClickHandler function returns a closure that has access to a private clickCount variable specific to each button's click event. When the buttons are clicked, the event handlers display the number of times each button is clicked, maintaining their individual clickCount values.

5. Function Currying: Closures can be used to implement function currying, which allows you to create specialized functions by partially applying arguments.

function curryAdd(a) {
return function(b) {
return a + b;
};
}

const addFive = curryAdd(5);
console.log(addFive(3)); // Output: 8 (5 + 3)

const addTen = curryAdd(10);
console.log(addTen(7)); // Output: 17 (10 + 7)

In this example, the curryAdd function returns a closure that partially applies the first argument a and returns a new function. The returned function can then be used to add a to any other number provided as the second argument.

6. Emulating Private Methods: Closures can be used to emulate private methods in JavaScript, allowing you to hide implementation details and expose only specific public methods.

const calculator = (function() {
function add(a, b) {
return a + b;
}

function subtract(a, b) {
return a - b;
}

return {
add,
// Subtract method is not directly accessible outside the closure
};
})();

console.log(calculator.add(5, 3)); // Output: 8
// console.log(calculator.subtract(5, 3)); // Error: calculator.subtract is not a function

In this example, the calculator object is created using an immediately invoked function expression (IIFE) that defines private functions add and subtract. Only the add method is exposed and accessible outside the closure, hiding the subtract method from direct access.

7. Caching Function Results with Memoization: Closures can be used to implement memoization to cache expensive function results.

function memoize(fn) {
const cache = {};

return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}

const result = fn(...args);
cache[key] = result;
return result;
};
}

function fibonacci(n) {
if (n <= 2) {
return 1;
}

return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFibonacci = memoize(fibonacci);
console.log(memoizedFibonacci(10)); // Output: 55 (calculated and cached)
console.log(memoizedFibonacci(15)); // Output: 610 (calculated and cached)
console.log(memoizedFibonacci(10)); // Output: 55 (retrieved from cache, not recalculated)

In this example, the memoize function takes a function fn as an argument and returns a closure that caches the results of the fn function using an object (cache) as a key-value store.

These additional examples showcase different practical use cases of closures, including event handlers with private data, function currying, emulating private methods, and memoizing function results. Closures offer a versatile and powerful mechanism in JavaScript for achieving various programming patterns and solving complex problems more efficiently.

8. Singleton Pattern: Closures can be used to implement the Singleton pattern, ensuring that a class has only one instance and providing a global point of access to that instance.

const Singleton = (function() {
let instance;

function createInstance() {
const obj = { key: 'value' };
return obj;
}

return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();

const singletonObj1 = Singleton.getInstance();
const singletonObj2 = Singleton.getInstance();

console.log(singletonObj1 === singletonObj2); // Output: true (both variables refer to the same instance)

In this example, the Singleton module returns a closure with a private instance variable. The createInstance function creates an object, and the getInstance method ensures that only one instance of the object is created and returned.

9. Callback with Persistent State: Closures can be used in scenarios where you need a callback function to maintain a persistent state between calls.

function createCounter() {
let count = 0;

return function() {
count++;
console.log(`Current count: ${count}`);
};
}

const counterCallback = createCounter();

counterCallback(); // Output: Current count: 1
counterCallback(); // Output: Current count: 2
counterCallback(); // Output: Current count: 3

In this example, the createCounter function returns a closure that maintains a private count variable. Each time you call the counterCallback, it increments the count and displays the current count value.

10. Event Handling with Unbinding: Closures can be used for event handling in scenarios where you need to unbind event listeners.

<button id="clickButton">Click Me</button>
function handleClick() {
let count = 0;

function onClick() {
count++;
console.log(`Button clicked ${count} times.`);

if (count === 5) {
// Unbind the event listener after 5 clicks
document.getElementById('clickButton').removeEventListener('click', onClick);
console.log('Event listener unbound.');
}
}

return onClick;
}

const clickButton = document.getElementById('clickButton');
clickButton.addEventListener('click', handleClick());

In this example, the handleClick function returns a closure onClick that increments the count each time the button is clicked. After 5 clicks, it unbinds the event listener from the button.

11. Caching Function Results with Multiple Arguments: Closures can be used to implement memoization for functions with multiple arguments.

function memoize(fn) {
const cache = {};

return function(...args) {
const key = args.join('_');
if (cache[key]) {
return cache[key];
}

const result = fn(...args);
cache[key] = result;
return result;
};
}

function multiply(a, b) {
console.log('Calculating...');
return a * b;
}

const memoizedMultiply = memoize(multiply);

console.log(memoizedMultiply(2, 3)); // Output: Calculating... \n 6
console.log(memoizedMultiply(2, 3)); // Output: 6 (retrieved from cache, not recalculated)

In this example, the memoize function takes a function fn as an argument and returns a closure that caches the results of the fn function based on the combination of arguments.

These additional examples demonstrate various practical use cases of closures in JavaScript, including the Singleton pattern, callbacks with persistent state, event handling with unbinding, caching function results, and more. Closures are a powerful feature that enables you to create more efficient and expressive code in many different scenarios.

12. Function Memoization with Time-to-Live (TTL): Closures can be used to implement function memoization with an expiration time for cached results.

function memoizeWithTTL(fn, ttl) {
const cache = {};

return function(...args) {
const key = JSON.stringify(args);
if (cache[key] && Date.now() - cache[key].timestamp < ttl) {
return cache[key].result;
}

const result = fn(...args);
cache[key] = { result, timestamp: Date.now() };
return result;
};
}

function expensiveFunction(n) {
console.log('Calculating...');
return n * 2;
}

const memoizedExpensiveFunction = memoizeWithTTL(expensiveFunction, 2000);

console.log(memoizedExpensiveFunction(5)); // Output: Calculating... \n 10
console.log(memoizedExpensiveFunction(5)); // Output: 10 (retrieved from cache, not recalculated)
// Wait for 2 seconds
setTimeout(() => console.log(memoizedExpensiveFunction(5)), 2000); // Output: Calculating... \n 10

In this example, the memoizeWithTTL function takes a function fn and a time-to-live (TTL) value in milliseconds. It returns a closure that caches the results of the fn function with an expiration time of ttl milliseconds.

13. Function Composition: Closures can be used for function composition, where you combine multiple functions to create a new function with specific behavior.

function add(a, b) {
return a + b;
}

function multiply(a, b) {
return a * b;
}

function compose(fn1, fn2) {
return function(...args) {
return fn1(fn2(...args));
};
}

const addAndMultiply = compose(multiply, add);

console.log(addAndMultiply(2, 3, 4)); // Output: 20 (add(2, 3) = 5, multiply(5, 4) = 20)

In this example, the compose function takes two functions fn1 and fn2 and returns a closure that applies the functions in reverse order when called.

14. Handling Private Data in Classes: Closures can be used to handle private data in JavaScript classes.

class Person {
constructor(name, age) {
let privateName = name;
let privateAge = age;

this.getName = function() {
return privateName;
};

this.getAge = function() {
return privateAge;
};
}

// Public method (prototype method)
sayHello() {
console.log(`Hello, my name is ${this.getName()} and I'm ${this.getAge()} years old.`);
}
}

const person = new Person('John', 30);
person.sayHello(); // Output: Hello, my name is John and I'm 30 years old.
// Private data cannot be accessed directly from outside the class
// console.log(person.privateName); // Output: undefined

In this example, the Person class uses closures to handle private data (privateName and privateAge) by defining private functions getName and getAge. The private data is accessible only through these functions, ensuring data encapsulation.

15. Currying with Multiple Arguments: Closures can be used to implement currying with functions that take multiple arguments.

function curry(fn, ...args) {
return function(...newArgs) {
const allArgs = [...args, ...newArgs];
if (allArgs.length >= fn.length) {
return fn(...allArgs);
}
return curry(fn, ...allArgs);
};
}

function add(a, b, c) {
return a + b + c;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // Output: 6

In this example, the curry function takes a function fn and curries it, allowing you to pass arguments one by one. Once all the required arguments are provided, the function is executed and returns the result.

These additional examples showcase different practical use cases of closures in JavaScript, including function memoization with TTL, function composition, handling private data in classes, currying with multiple arguments, and more. Closures offer a versatile and powerful mechanism in JavaScript for achieving various programming patterns and solving complex problems more efficiently.

16. Managing Dependencies in a Modular System: Closures can be used in modular systems to manage dependencies and avoid polluting the global namespace.

const myModule = (function() {
// Private variables
let counter = 0;

// Private function
function increment() {
counter++;
}

// Public interface
return {
getCount: function() {
return counter;
},
incrementCount: function() {
increment();
}
};
})();

myModule.incrementCount();
console.log(myModule.getCount()); // Output: 1

17. Currying with Multiple Arguments and Dynamic Arity: Closures can be used to implement currying with functions that have a dynamic number of arguments.

function curry(fn, ...args) {
return function(...newArgs) {
const allArgs = [...args, ...newArgs];
if (allArgs.length >= fn.length) {
return fn(...allArgs);
}
return curry(fn, ...allArgs);
};
}

function add(...args) {
return args.reduce((total, num) => total + num, 0);
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)()); // Output: 6
console.log(curriedAdd(1, 2)(3)()); // Output: 6
console.log(curriedAdd(1, 2, 3)()); // Output: 6

In this example, the curry function allows the add function to be curried with a dynamic number of arguments. The function can be called with one or more arguments, and the final result is returned when an empty set of parentheses is invoked.

These additional examples showcase different practical use cases of closures in JavaScript, including implementing a counter, managing dependencies in modular systems, handling persistent state in event handlers, creating a simple pub/sub system, and currying with dynamic arity. Closures offer a versatile and powerful mechanism in JavaScript for achieving various programming patterns and solving complex problems more efficiently.

18. Private Static Variables in a Class: Closures can be used to create private static variables in a class, shared among all instances of the class.

class Counter {
static create() {
let count = 0;

return {
increment: function() {
count++;
},
decrement: function() {
count--;
},
getCount: function() {
return count;
}
};
}
}

const counter1 = Counter.create();
const counter2 = Counter.create();

counter1.increment();
counter1.increment();
counter2.increment();
console.log(counter1.getCount()); // Output: 2
console.log(counter2.getCount()); // Output: 1

In this example, the static method create of the Counter class returns a closure that maintains a private count variable shared among all instances of the class.

19. Implementing a Simple Debounce Function: Closures can be used to implement a simple debounce function that delays the execution of a function until after a certain period of inactivity.

function debounce(func, delay) {
let timeoutId;

return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}

function handleInput(value) {
console.log('Input:', value);
}

const debouncedInputHandler = debounce(handleInput, 500);

debouncedInputHandler('Hello');
debouncedInputHandler('H');
debouncedInputHandler('He');
debouncedInputHandler('Hel');
debouncedInputHandler('Hell');
debouncedInputHandler('Hello'); // Only this log will be executed after 500ms

In this example, the debounce function takes a function func and a delay time. It returns a closure that waits for the specified delay period before invoking the function, ensuring that the function is executed only after a pause in activity.

20. Creating a Namespace for a Library or Utility: Closures can be used to create a namespace for a library or utility to avoid global namespace pollution.

const MyLibrary = (function() {
// Private variables and functions
const privateVar = 42;

function privateFunction() {
console.log('This is a private function.');
}

// Public interface
return {
publicVar: 'Hello',
publicFunction: function() {
console.log('This is a public function.');
}
};
})();

console.log(MyLibrary.publicVar); // Output: Hello
MyLibrary.publicFunction(); // Output: This is a public function.
// These will cause errors as private members are not directly accessible
// console.log(MyLibrary.privateVar);
// MyLibrary.privateFunction();

In this example, the IIFE (Immediately Invoked Function Expression) creates a closure to enclose private variables and functions. The returned object exposes only the public members of the library.

21. Implementing a Throttle Function: Closures can be used to implement a throttle function that limits the execution rate of a function to a specified interval.

function throttle(func, delay) {
let lastExecutionTime = 0;

return function(...args) {
const now = Date.now();
if (now - lastExecutionTime >= delay) {
func.apply(this, args);
lastExecutionTime = now;
}
};
}

function handleScroll() {
console.log('Scroll event triggered.');
}

const throttledScrollHandler = throttle(handleScroll, 1000);

window.addEventListener('scroll', throttledScrollHandler);

In this example, the throttle function takes a function func and a delay time. It returns a closure that ensures the function is executed at most once per delay milliseconds.

These additional examples showcase different practical use cases of closures in JavaScript, including creating private static variables in a class, implementing a simple debounce function, creating a namespace for a library, implementing a throttle function, and more. Closures offer a versatile and powerful mechanism in JavaScript for achieving various programming patterns and solving complex problems more efficiently.

22. Function Chaining with Closures: Closures can be used to enable function chaining, where multiple methods are chained together on an object.

function Calculator() {
let result = 0;

return {
add: function(num) {
result += num;
return this;
},
subtract: function(num) {
result -= num;
return this;
},
multiply: function(num) {
result *= num;
return this;
},
divide: function(num) {
result /= num;
return this;
},
getResult: function() {
return result;
}
};
}

const calc = new Calculator();
const result = calc.add(5).multiply(3).subtract(2).divide(2).getResult();
console.log(result); // Output: 8

In this example, the Calculator constructor function returns an object with methods that allow chaining of operations on the calculator.

These additional examples showcase different practical use cases of closures in JavaScript, including partial application of functions, handling private data in objects, memoization with variable arity, function chaining, and more. Closures are a powerful feature that enables you to create more expressive and efficient code in many different scenarios.

23. Implementing a Retry Mechanism: Closures can be used to implement a retry mechanism for functions that might fail temporarily due to network issues or other reasons.

function retryOperation(operation, maxRetries, delay) {
return function tryOperation(attempt) {
return operation()
.catch(error => {
if (attempt < maxRetries) {
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
return new Promise(resolve => setTimeout(resolve, delay)).then(() => tryOperation(attempt + 1));
}
throw error;
});
}(0);
}

function fetchUserData() {
const randomNumber = Math.random();
if (randomNumber < 0.8) {
return Promise.resolve({ name: 'John', age: 30 });
} else {
return Promise.reject(new Error('Failed to fetch data.'));
}
}

retryOperation(fetchUserData, 3, 1000)
.then(data => console.log('User Data:', data))
.catch(error => console.error('Error:', error.message));

In this example, the retryOperation function takes an operation, maximum number of retries, and delay time in milliseconds. It returns a closure that retries the operation until it succeeds or the maximum number of retries is reached.

24. Implementing a Private Cache with Time-to-Live (TTL): Closures can be used to implement a private cache with a time-to-live (TTL) for caching expensive function results.

function createCache(ttl) {
const cache = {};

return function memoize(fn, ...args) {
const key = JSON.stringify(args);
if (cache[key] && Date.now() - cache[key].timestamp < ttl) {
return cache[key].result;
}

const result = fn(...args);
cache[key] = { result, timestamp: Date.now() };
return result;
};
}

function expensiveFunction(n) {
console.log('Calculating...');
return n * 2;
}

const cacheWithTTL = createCache(2000);
const memoizedExpensiveFunction = cacheWithTTL(expensiveFunction);

console.log(memoizedExpensiveFunction(5)); // Output: Calculating... \n 10
console.log(memoizedExpensiveFunction(5)); // Output: 10 (retrieved from cache, not recalculated)
// Wait for 2 seconds
setTimeout(() => console.log(memoizedExpensiveFunction(5)), 2000); // Output: Calculating... \n 10

In this example, the createCache function takes a time-to-live (TTL) value and returns a closure memoize. The memoize closure memoizes the results of the function with a cache that expires after the specified TTL.

24. Building a Simple Event Emitter: Closures can be used to create a simple event emitter that allows subscribing to and emitting custom events.

function createEventEmitter() {
const listeners = {};

function on(eventName, callback) {
if (!listeners[eventName]) {
listeners[eventName] = [];
}
listeners[eventName].push(callback);
}

function emit(eventName, ...args) {
if (listeners[eventName]) {
listeners[eventName].forEach(callback => callback(...args));
}
}

return {
on,
emit
};
}

const emitter = createEventEmitter();

emitter.on('hello', name => console.log(`Hello, ${name}!`));
emitter.emit('hello', 'John'); // Output: Hello, John!
emitter.emit('hello', 'Alice'); // Output: Hello, Alice!

25. Lazy Initialization of Complex Data: Closures can be used to implement lazy initialization of complex data structures, creating them only when needed.

function createComplexData() {
let data = null;

return function getData() {
if (data === null) {
// Perform complex data initialization
data = { /* ...complex data... */ };
}
return data;
};
}

const getComplexData = createComplexData();

console.log(getComplexData()); // Output: { ...complex data... }
console.log(getComplexData()); // Output: { ...complex data... } (retrieved from cache, not recalculated)

In this example, the createComplexData function returns a closure getData that initializes the complex data only when it is requested for the first time. Subsequent calls to getData retrieve the previously calculated data from the closure.

These additional examples showcase different practical use cases of closures in JavaScript, including implementing a retry mechanism, creating a private cache with TTL, building a simple event emitter, lazy initialization of complex data, and more. Closures offer a versatile and powerful mechanism in JavaScript for achieving various programming patterns and solving complex problems more efficiently.

--

--