JavaScript Closures

I am a software developer, passionate about developing and designing solutions with an emphasis on great user experience.
For as long as I can remember, JavaScript closures have always seemed to be a hard concept to grasp. Yet, without realizing it, I often came across closures in JavaScript code. From event handlers to currying, closures are common in JavaScript.
But what exactly is a closure? And why is it worth knowing about?
In this article, we’ll demystify closures with simple explanations and practical examples, which will hopefully help in enhancing your JavaScript knowledge or preparing for an interview.
Let’s dive in and uncover how JavaScript functions can “remember” the environment where they were created—and how that superpower enables cleaner, more modular, and more expressive code.
Here’s what we’ll cover:
Lexical Scoping
To understand closures, let’s first look at lexical scoping. Let’s consider the following example code:
function init() {
const name = "Mary";
function displayGreeting(){
console.log(`Hi, I am ${name}`);
}
displayGreeting();
}
init();
init has a local variable name and a function displayGreeting, which is an inner function and only available within the init function. Inner functions can access variables from the outer scope, so displayGreeting can access the name variable and displays “Hi, I am Mary” in the console.
This is an example of lexical scoping. Lexical scoping means that the place where a variable is declared in the source code determines where that variable can be used. This information is helpful when working with nested functions as they have access to variables declared in the outer scope.
Closure
Consider the following code example:
function createHuman(name){
function displayGreeting(){
console.log(`Hi, I am ${name}`);
}
return displayGreeting;
}
const human1 = createHuman("John");
human1(); // will display Hi, I am John
As mentioned in the previous example about lexical scoping, displayGreeting has access to the name variable, which is passed as a parameter in the createHuman function, its outer scope. When we call createHuman("John"), displayGreeting is returned and still remembers the name variable. So, when we call human1, it displays "Hi, I am John" because displayGreeting still remembers that name = "John".
This is essentially what a closure is. It allows an inner function to access an outer function’s scope and remember the variables in the outer scope, even after the outer function has finished running.
Let's see how this works in nested functions:
function createHuman(name, mood){
function displayGreeting(){
console.log(`Hi, I am ${name}`);
}
function displayMood(){
console.log(`${name} is ${mood}!`);
}
return { displayGreeting, displayMood };
}
const human1 = createHuman("John", "happy");
const human2 = createHuman("Mary", "sad");
human1.displayMood(); // displays John is happy!
human2.displayMood(); // displays Mary is sad!
In the example above, we can create methods in an object and call them when needed. Even though both use the same function definitions, each one retains its own memory (closure) of the variables it was created with. This is useful because it allows logic to be reused and objects with custom behavior to be built in a clean and functional way.
Practical use cases of closures
Closures are useful in preserving data, encapsulating state, and creating powerful and flexible functions. Let’s take a look at a few examples and explanations:
Encapsulation
Using closures, you can emulate private methods like those in languages such as Java and C#. Closures allow the creation of private variables that cannot be accessed directly from outside the function.
function createCounter() {
let count = 0;
return {
increment() {
count++;
},
decrement() {
count--;
},
getValue() {
return count;
}
};
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1.increment();
counter1.increment();
console.log(counter1.getValue()); // 2
counter1.decrement();
console.log(counter1.getValue()); // 1
console.log(counter2.getValue()); // 0
count is private, so it can only be accessed indirectly through the three public functions: increment, decrement, and getValue. It's also important to note that the two counters remain independent, and the value of each counter is different based on its own lexical environment.
Using closures in this way provides benefits associated with object-oriented programming in regards to encapsulation.
Sharing data with event handlers and callback
Suppose we want to adjust the font size based on button clicked, for example:
document.getElementById('size-12').onclick = function () {
document.body.style.fontSize = '12px'
}
document.getElementById('size-14').onclick = function () {
document.body.style.fontSize = '14px'
}
Rather than creating multiple on-click handlers, we can create an inner function (closure) which can be attached to the buttons.
function clickHandler(size){
return function() {
document.body.style.fontSize = `{size}px`;
}
}
document.getElementById('size-12').onclick = clickHandler(12);
document.getElementById('size-14').onclick = clickHandler(14);
Currying and partial applications
Currying is a technique where a function with multiple arguments is transformed into a series of functions, each taking a single argument. This allows you to call a function with fewer arguments than it expects, returning a new function that takes the remaining arguments.
function curriedMultiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}
const double = curriedMultiply(2); // Closure remembers a = 2
console.log(double(5)(10)); // returns 100
Partial application is a technique where you prefill one or more arguments of a function, returning another function that takes the remaining arguments.
function multiply(a, b, c) {
return a * b * c;
}
function partialMultiply(a) {
return function(b, c) {
return multiply(a, b, c);
};
}
const multiplyBy2 = partialMultiply(2); // Closure remembers a = 2
console.log(multiplyBy2(3, 4)); // 24
Conclusion
Closures in JavaScript are a fundamental concept that allow a function to access variables from its outer scope, even after the outer function has finished executing. This special behavior allows for useful patterns like data encapsulation, maintaining state between function calls, and creating partially applied or curried functions. By understanding and leveraging closures, developers can write more modular, reusable, and expressive code, making them an essential tool for building robust JavaScript applications.



