DEV Community

Cover image for Understanding Closures in JavaScript
Varun Kelkar
Varun Kelkar

Posted on • Edited on

Understanding Closures in JavaScript

Table of Contents

What's a closure

Why are they needed

Encapsulation & State Management

Higher Order Functions

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

  1. Modular & Reusable Code.
  2. 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?
  • 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.


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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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.

What are Pure Functions?

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.

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 );
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)