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, just by reading the definition, closures never clicked for me ๐.
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 ( aka Scope Management )
This is exactly where closure is needed.
For simplicity, let's say functional programming is all about thinking in terms of functions & smartly leveraging them in our code.
Functions are treated as first class citizens. They can be declared as a variable, can be passed as arguments, can be returned from another function & much more.
Let's dive into some examples.
Encapsulation
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.
First let's see OOPS way of achieving 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
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 the definition of closure 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
.
This way we've achieved encapsulation 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.
// 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 even if forEach
finished its execution first.
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.
Use-case
Let's say we have to write a function which calculates bill.
The conditions are,
1] We give 10% discount if amount is greater than 1000.
2] 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 manyif & else blocks
orcode 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 };
I hope you understood how cool the closures are ๐
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!