10 use cases of closures in JavaScript

Mayank Choubey
Tech Tonic
Published in
11 min readMar 31, 2024

--

In JavaScript, closures are a powerful concept that uses function scoping to create functions with “memory.” Unlike some languages, JavaScript functions don’t inherently have private variables. Closures address this by creating an association between a function and the variables from the function’s enclosing scope, even after the outer function has returned.

Imagine a function creating another function within it. The inner function can access variables defined in the outer function’s scope, even if the outer function has finished executing. This “remembers” the state of the outer function, allowing the inner function to work with private data or maintain context.

Closures have numerous applications. They enable data encapsulation, simulating private variables within functions. This promotes data protection and modularity. Closures are also essential for event handlers in web development. By creating event handlers within functions that have access to specific data, you can ensure the handlers retain that data for personalized interactions. Finally, closures play a role in creating module-like structures and function currying, a technique for creating functions that accept their arguments in parts.

In this article, we’ll look at 10 use cases of closures with code samples.

Use case 1 — Data encapsulation and private variables

Unlike some object-oriented languages, JS functions don’t inherently have private variables. Closures provide a mechanism to achieve a similar effect. An inner function can access variables defined in its outer function’s scope, even after the outer function has returned. This allows us to create a function that has access to private data, effectively encapsulating that data and preventing unintended modification from external code.

function createCounter() {
let count = 0;

function increment() {
count++;
return count;
}

return increment;
}

const counter1 = createCounter();
const counter2 = createCounter();

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

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

// count variable is not accessible here because it's private to the closure
// console.log(count); // ReferenceError

The createCounter function defines a variable count and an inner function increment. The inner function increment can access and modify the count variable because it’s part of the closure created when createCounter is executed. When createCounter is called, it returns the increment function, effectively hiding the count variable from the outside world.

Multiple calls to createCounter will result in separate closures, each with its own private count variable. This ensures that each instance of the counter maintains its own independent state.

Use case 2 — Event handlers with preserved state

JavaScript heavily relies on event handling for interactive web applications. When attaching event listeners to DOM elements, we often need the handler function to access data specific to the element or its context. Closures come in handy for this scenario. By creating an event handler function within another function that has access to relevant data, we can ensure the handler retains that data even after the outer function has executed.

function createGreeter(name) {
function greet() {
console.log("Hello, " + name + "!");
}

return greet;
}

const button1 = document.getElementById("button1");
const button2 = document.getElementById("button2");

button1.addEventListener("click", createGreeter("Alice"));
button2.addEventListener("click", createGreeter("Bob"));

// Clicking button1 or button2 will now log the corresponding greeting message

The createGreeter function takes a name argument and defines an inner function greet. When createGreeter is called with a name, it creates a closure that captures the specific name value. The greet function is then returned, allowing it to be assigned as an event handler.

When a button is clicked, the corresponding event handler function (returned by createGreeter) is executed. Since the handler function is part of a closure, it has access to the captured name variable, even though createGreeter itself has already finished executing. This ensures that each button click triggers the appropriate greeting message with the specific name.

Use case 3 — Modules and function currying

JavaScript doesn’t have built-in modules like some other languages. However, closures can be used to create a module-like structure that groups functions and variables together while controlling their accessibility. Additionally, closures enable a technique called currying, which allows a function to accept its arguments in parts.

function createMathModule() {
const PI = 3.14159;

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

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

// Function currying example: create a function for calculating area of a circle
function circleArea(radius) {
return function(multiplier = 1) { // Default multiplier of 1 for area
return PI * Math.pow(radius, 2) * multiplier;
}
}

return {
add,
multiply,
circleArea,
};
}

const mathModule = createMathModule();

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

const circleAreaCalculator = mathModule.circleArea(5);
console.log(circleAreaCalculator()); // Output: 78.53975 (area)
console.log(circleAreaCalculator(2)); // Output: 157.0795 (circumference with multiplier 2)

// PI variable is not directly accessible here because it's private to the closure
// console.log(PI); // ReferenceError

The createMathModule function defines a constant PI and two mathematical functions add and multiply. These functions and the constant are encapsulated within the closure created when createMathModule is called. The function then returns an object containing references to the desired functions, effectively creating a private namespace for them.

The circleArea function demonstrates currying. It takes a radius argument and returns another function. This inner function can be called with an optional multiplier argument. This allows for reusability: the same function can calculate both area (with multiplier 1) and circumference (with a different multiplier).

Use case 4 — Memoization

Memoization is an optimization technique that stores the results of function calls to avoid redundant calculations. Closures are well-suited for implementing memoization because they allow functions to remember previously computed values based on their arguments.

function fibonacciMemoized(n) {
const cache = {};

function fibonacci(n) {
if (n === 0 || n === 1) {
return n;
}

if (cache[n]) {
return cache[n];
}

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

return fibonacci(n);
}

console.log(fibonacciMemoized(5)); // Output: 5 (calculated and stored in cache)
console.log(fibonacciMemoized(5)); // Output: 5 (retrieved from cache, no recalculation)

The fibonacciMemoized function creates a closure that stores a cache object (cache) to hold previously calculated Fibonacci numbers. The inner function fibonacci takes an n argument and checks for base cases (0 and 1).

If n is not a base case and the result isn’t already in the cache (cache[n]), the function calculates the Fibonacci value recursively using fibonacci(n — 1) and fibonacci(n — 2). The calculated result is then stored in the cache for future reference. Finally, the function returns the Fibonacci value.

Use case 5 — Partial application

Partial application is a technique where a function with multiple arguments is converted into a sequence of functions, each taking a single argument. This allows us to pre-set some arguments of a function while leaving others open for later invocation. Closures are instrumental in achieving partial application because they allow the creation of new functions that capture a specific set of arguments from the original function.

function formatMoney(prefix = "$", precision = 2) {
return function(number) {
return prefix + number.toFixed(precision);
}
}

const usdFormatter = formatMoney("$", 2);
const eurFormatter = formatMoney("€", 3);

console.log(usdFormatter(123.456)); // Output: $123.46
console.log(eurFormatter(789.0123)); // Output: €789.012

The formatMoney function takes two optional arguments: prefix (defaulting to “$”) and precision (defaulting to 2). It returns an inner function that takes a single argument (number). This inner function uses the captured values of prefix and precision from the closure to format the provided number with the specified prefix and number of decimal places.

By calling formatMoney with specific values for prefix and precision, we create new functions like usdFormatter and eurFormatter. These new functions are partially applied versions of the original formatMoney function, with the currency symbol and number of decimal places already set.

Use case 6 — Namespaces and avoiding variable collisions

In JavaScript, variables declared at the global scope are accessible throughout the entire application. This can lead to naming conflicts, especially in large projects with multiple developers. Closures provide a mechanism to create private namespaces, effectively isolating variables and functions within a specific scope.

function createNamespace(name) {
const namespace = {};

function addToNamespace(key, value) {
namespace[key] = value;
}

return {
addToNamespace,
// Functions or variables specific to the namespace can be added here
greet: function() {
console.log("Hello from " + name + " namespace!");
}
};
}

const mathNamespace = createNamespace("Math");
mathNamespace.addToNamespace("PI", 3.14159);

const utilNamespace = createNamespace("Util");
utilNamespace.addToNamespace("formatDate", function(date) {
// Date formatting logic here
});

mathNamespace.greet(); // Output: Hello from Math namespace!
// console.log(mathNamespace.PI); // Would cause an error as PI is private

utilNamespace.formatDate(new Date()); // Calls the date formatting function

The createNamespace function takes a name argument and creates a closure. Inside the closure, an empty object namespace is used to store variables and functions specific to that namespace. The function addToNamespace allows adding key-value pairs to the namespace object.

When createNamespace is called with a name (e.g., “Math”), it returns an object containing the addToNamespace function and any functions or variables defined within the closure (e.g., the greet function). This object acts as the namespace itself.

Use case 7 — Iterators and generators

JavaScript iterators and generators are powerful tools for working with sequences of data. Closures play a crucial role in their implementation. Iterators provide a way to access elements of a collection one at a time, while generators are functions that can pause execution and yield values on demand.

function createNumberIterator(start, end) {
let current = start - 1;

function next() {
current++;
if (current <= end) {
return { value: current, done: false };
} else {
return { done: true };
}
}

return {
next,
};
}

const numberIterator = createNumberIterator(1, 5);

console.log(numberIterator.next()); // Output: { value: 1, done: false }
console.log(numberIterator.next()); // Output: { value: 2, done: false }
// ... call next() repeatedly to iterate through numbers

// Generator function example (similar concept with yield)
function* generatePrimes(max) {
let num = 2;
while (num <= max) {
yield num;
num++;
// Prime number checking logic here (simplified for brevity)
}
}

const primeGenerator = generatePrimes(10);

console.log(primeGenerator.next()); // Output: { value: 2, done: false }
console.log(primeGenerator.next()); // Output: { value: 3, done: false }
// ... call next() repeatedly to get prime numbers

The createNumberIterator function takes a start and end value. It uses a closure to keep track of the current iteration (current) through a variable declared within the function. The next function increments current and checks if it’s within the specified range. If so, it returns an object with the current value and done set to false. Otherwise, it returns an object with done set to true, signaling the end of the iteration.

The second code snippet demonstrates a generator function generatePrimes. Generator functions use the yield keyword to pause execution and return a value. When next is called on the generator object (primeGenerator), the function resumes execution until the next yield statement, returning the current prime number. This allows for efficient generation of sequences on demand, especially for infinite or very large datasets.

In both cases, closures are essential. The iterator and generator functions maintain their state (like current or the prime checking logic) within the closure, allowing them to remember their progress and resume from where they left off when next is called again.

Use case 8 — Function factories with configuration

JavaScript functions can be quite versatile. Closures allow us to create function factories that generate new functions with customized behavior based on initial configuration. This pattern is useful for creating functions with reusable core logic but configurable options.

function createLogger(logLevel = "info") {
const logLevels = {
debug: console.debug,
info: console.info,
warn: console.warn,
error: console.error,
};

function log(message) {
if (logLevels[logLevel] && logLevel !== "debug" || logLevel === "debug" && process.env.NODE_ENV !== "production") {
logLevels[logLevel](message);
}
}

return log;
}

const infoLogger = createLogger("info");
const errorLogger = createLogger("error");
const debugLogger = createLogger("debug"); // Only logs in non-production environment

infoLogger("Informational message");
errorLogger("Error message!");

// Debug message won't be logged unless process.env.NODE_ENV is set to something other than "production"
debugLogger("Debug message");

The createLogger function takes an optional logLevel argument (defaulting to “info”). It defines an object logLevels that maps different log levels to their corresponding console functions.

Inside the closure, the log function takes a message argument. It checks the configured logLevel and the current environment (process.env.NODE_ENV) to determine if the message should be logged. This allows for filtering based on the log level and preventing debug messages in production environments.

Finally, the function returns the log function itself. By calling createLogger with different log levels, we create new logger functions with pre-configured behavior.

Use case 9 — Module augmentation

JavaScript modules allow for code organization and encapsulation. However, there might be situations where we want to extend the functionality of an existing module without modifying its original code. Closures come in handy for this purpose, enabling a technique called module augmentation.

// Original module (mathModule.js)
export function add(a, b) {
return a + b;
}

export function subtract(a, b) {
return a - b;
}
// Module augmentation (app.js)
import * as math from "./mathModule.js";

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

// Augment the math module by adding the multiply function within a closure
(function(math) {
math.multiply = multiply;
})(math);

console.log(math.add(5, 3)); // Output: 8 (from original module)
console.log(math.multiply(4, 2)); // Output: 8 (added functionality)

// Original mathModule.js remains unchanged

The first file (mathModule.js) defines a module with two functions: add and subtract. These functions are exported for use in other modules. The second file (app.js) imports the entire mathModule using import * as math from “./mathModule.js”. It then defines a new function multiply for multiplication. The key part is the Immediately Invoked Function Expression (IIFE). This anonymous function wraps the code that augments the math module. It takes the imported math object as an argument.
Inside the IIFE, a new property multiply is added to the math object, effectively extending its functionality with the new function.

In this scenario, the closure plays a crucial role. The IIFE creates a private scope for the augmentation logic. By assigning the multiply function to math.multiply within the closure, we extend the functionality of the imported math module without modifying its original code.

Use case 10 — Debouncing and throttling user input

In web applications, handling user interactions like key presses or rapid clicks efficiently is crucial. Closures are instrumental in implementing techniques like debouncing and throttling to optimize performance and prevent unintended behavior.

Debouncing delays the execution of a function until a certain amount of time has passed since the last call. This is useful for scenarios like search bars where we only want to trigger a search after the user finishes typing, not after every keystroke.

Throttling limits the execution of a function to a specific number of times within a given time interval. This is helpful for preventing excessive function calls during rapid user actions, like resizing a window or scrolling a large list.

function debounce(func, delay) {
let timeout;

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

const searchInput = document.getElementById("searchInput");

const debouncedSearch = debounce(function(searchTerm) {
console.log("Search for:", searchTerm);
}, 500); // Delay execution by 500ms after last keystroke

searchInput.addEventListener("keyup", function(event) {
debouncedSearch(event.target.value);
});

// User types "h" then waits...
// User types "e" then waits... (delays execution)
// After 500ms of inactivity, the search function is called with the entire search term "he"

The debounce function takes a function (func) and a delay time (delay) as arguments. It uses a closure to store a timeout reference (timeout). Whenever the debounced function is called, it clears any existing timeout and sets a new one. This ensures that the actual func is only called after the specified delay (delay) has passed since the last call, effectively grouping rapid user input into a single execution.

In the example, the debouncedSearch function delays the search logic by 500 milliseconds after the user stops typing. This prevents unnecessary API calls or computations for every keystroke.

Throttling can be implemented using a similar approach with a flag to track if the function is currently executing within the allowed time interval.

Thanks for reading this article! I hope this has been helpful in learning something new.

--

--