Closures in JavaScript: What They Are and Why They Matter

ยท

6 min read

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

  1. Modular & Reusable Code.

  2. 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.

๐Ÿ’ก
Functional Programming is easier to understand with examples. So i'll provide lots of them.

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

  1. We have a variable count & we want to allow limited operations on it - increment, decrement, reset & get .

  2. 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.

๐Ÿ’ก
Pure Functions - functions which return consistent output provided it receives consistent input. They cause no side-effects & make code more predictable.

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.

  1. 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.

  2. 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!

ย