Understanding Closures in JavaScript
Varun Kelkar
Posted on June 3, 2024
Table of Contents
Encapsulation & State Management
Function Currying & Composition
How Javascript libraries leverage closures
In the vast world of JavaScript, closures stand out as one of the most powerful and intriguing concepts. Whether you're a seasoned developer or just starting your coding journey, understanding closures is essential for mastering JavaScript.
In this blog post, we'll demystify closures, exploring their fundamental principles, practical applications, and why they are indispensable in modern JavaScript development.
By the end, you'll have a clear understanding of how closures work and how to leverage them to enhance your coding skills.
What's a closure ?
A closure is a fundamental concept in JavaScript (and many other programming languages) that allows a function to retain access to its lexical scope, even after the function that created the scope has finished executing.
To be honest, the definition does not give an idea about the power of closures๐.
So what if a function can retain its lexical scope ? What's the big deal ?
Believe Me โค๏ธ , closures are an infinity stone in the gauntlet of functional programming ๐ฎ
Why are they needed ?
In modern day code development, Functional Programming is highly leveraged because it has certain advantages over OOPS in certain areas.
With this change in approach, we still needed to support basic features of OOPS & clean coding.
Functional Programming has it's unique ways of implementing these features
- Modular & Reusable Code.
- Encapsulation & State Management.
This is exactly where closure is needed.
๐กFunctional Programming is easier to understand with examples. So i'll provide lots of them.
What is functional programming?
Encapsulation & State Management
Functional Programming has no concept of access specifiers - public, private & protected.
So how do we achieve encapsulation?
Closure
is one of the ways you can achieve encapsulation.
Use-case
- We have a variable count & we want to allow limited operations on it -
increment, decrement, reset & get.
- We want to prevent count from external access i.e. keep it private.
First let's see OOPS way of achieving encapsulation.
Encapsulation using OOPS
class Count {
private _count = 0;
function increment(){
this._count++;
}
function decrement(){
this._count--;
}
function reset(){
this._count = 0;
}
function getCount(){
return this._count;
}
}
const count = new Count();
Encapsulation using Functional Programming
function count() {
let count = 0;
return {
increment: function(){
count++;
},
decrement: function(){
count--;
},
reset: function(){
count = 0;
},
getCount: function(){
return count;
}
};
};
const fpCount = count();
Now does this statement ring a bell ??? Let's see...
Closure allows a function to retain access to its lexical scope, even after the function that created the scope has finished executing
.
When we called count()
it executed & returned us methods to play around with count variable.
But even after its execution, all handler methods remember value of count because they have access to their lexical environment.
Also using closures
the handler functions manage the state of count variable.
This way we've achieved encapsulation & state management of count variable ๐๐
Higher Order Functions
A higher-order function is a function that either Takes one or more functions as arguments or Returns a function as its result.
The purpose of Higher Order Functions is to make the code modular & reusable.
Use-case
- We want to loop over an array
// Classical way to loop over an array
const numbers = [1,2,3,4,5,6];
for(let i = 0; i < numbers.length; i++){
console.log('number ', numbers[i], ' is at index ', i);
}
// Same using functional programming
function printElementAndIndex(element, index) {
console.log('number ', element, ' is at index ', index);
}
// For any beginners reading this,
// forEach is a higher order function provided by javascript
numbers.forEach(printElementAndIndex);
You see printElementAndIndex
was called for every element in numbers array.
It remembered the values of element & index passed to it at that iteration.
- Attaching event handler
// Assuming we're attaching a click listener
// on button with id 'my-button'
const button = document.getElementById('#my-button');
const handleButtonClick = (event) => {
// handle click event here
}
// 'addEventListener' is the higher order function here
// because it accepts function as an argument
button.addEventListener('click',handleButtonClick);
addEventListener
finishes its execution the moment UI is rendered.
handleButtonClick
is a callback which executes later but it still gets access to Event
object because of closure.
Function Currying & Composition
Function Currying is a technique using which a function with multiple arguments is transformed into a series of functions each taking one argument.
Function Composition is a technique using which we can combine two or more functions to create custom functions.
These techniques also help us write Modular & Reusable code
by leveraging Pure Functions
which is one of the fundamental advantages of using Functional Programming.
Functions which return consistent output provided it receives consistent input. They cause no side-effects, do not modify any external state & make code more predictable.What are Pure Functions?
Use-case
- Let's say we have to write a function which calculates bill.
The conditions are,
We give 10% discount if amount is greater than 1000.
We levy 7% service charge on total amount.
This is how we could've written it without functional programming.
function calculateBill(amount) {
let totalAmount = amount;
if(totalAmount > 1000){
totalAmount = totalAmount - ( totalAmount * 10 / 10 );
}
return totalAmount + ( totalAmount * 7 / 100 );
}
Although this is a working function, it has some pitfalls.
Values of service charge & discount are hardcoded. In future if we have multiple discount offers depending on amount it's hard to adapt. Either too many if & else blocks or code duplication if separate function for each condition is created.
It's not a pure function.
Now let's write the same using Currying
& Composition
.
// Function Currying
// Notice how we split into two funtions
// Each handles one argument & makes function modular & reusable
function calculateDiscount(discountPercentage, eligibleAmount){
return function(amount){
if(amount > eligibleAmount) {
return amount - ( amount * discountPercentage / 100 );
}
return amount;
}
}
function addServiceCharge(taxPercentage){
return function(amount){
return amount + ( amount * taxPercentage / 100 );
}
}
// Later on if we change discount & tax percentages,
// we can quickly adapt.
// Notice that these are pure functions.
const discountByTen = calculateDiscount(10, 1000);
const levyServiceChargeOf7 = addServiceCharge(7);
// Function Composition
// Notice how we combined existing functions to create new function
const composeBillCalculator = (levyServiceCharge, applyDiscount)
=> amount => levyServiceCharge(applyDiscount(amount));
// Currently we have,
// 1] 10% discount
// 2] 7% service charge
const calculateBill =
composeBillCalculator(levyServiceChargeOf7, discountByTen);
export { calculateBill };
How Javascript libraries leverage closures
React
React, a popular frontend javascript library leverages closures to manage state of the component.
Also it has a concept of Hooks which entirely based on closures.
import React, { useState, useEffect } from 'react';
function Timer() {
// useState leverages closure internally.
// setSeconds is a setter to update state.
// seconds acts like a getter.
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
return () => clearInterval(interval); // Cleanup
}, []); // Empty dependency array to run once
return <div>{seconds} seconds have passed.</div>;
}
Nodejs
Nodejs, a popular backend javascript runtime, leverages closures when managing asynchronous execution via callbacks.
Use-case
- File read operation
const fs = require('fs');
function readFile(filePath) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log('File contents:', data);
});
}
readFile('example.txt');
The callback
function inside fs.readFile
forms a closure that retains access to filePath
and any other outer variables.
This closure ensures the callback can use these variables even after the asynchronous file read operation completes.
ExpressJs is a popular framework for Nodejs.
It has a concept called middlewares which uses closures.
I hope you understood how cool the closures are ๐
Do let me know in comments if you know any other usage of closures.
Go ahead leverage them in your code with confidence ๐
Additionally, closures are a popular topic in technical interviews. With the knowledge and examples provided here, you should be well-equipped to explain and demonstrate closures with confidence during your next interview.
Thank you for reading, and happy coding!
Posted on June 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024