DEV Community

Cover image for JavaScript Closures — Part 2 (Advanced Concepts & Real-World Applications)
Abanoub Kerols
Abanoub Kerols

Posted on

JavaScript Closures — Part 2 (Advanced Concepts & Real-World Applications)

In the previous part, we learned that a closure is simply:

A function that remembers the variables from the environment where it was created.

Now let's move beyond the basics and see how professional JavaScript developers use closures every day.


Real-World Closures

If you've written JavaScript for a while, you've already used closures—even if you didn't realize it.

Closures appear in:

  • Event Listeners
  • Timers (setTimeout, setInterval)
  • Promises
  • Async/Await
  • React Hooks
  • Express Middleware
  • Module Pattern
  • Factory Functions
  • Memoization
  • Functional Programming
  • Currying
  • Dependency Injection

Closures are one of the foundations of modern JavaScript.


Closures in Event Listeners

Suppose we have a button.

<button id="save">Save</button>
Enter fullscreen mode Exit fullscreen mode

Now imagine this JavaScript:

function setupButton() {

    let clickCount = 0;

    const button = document.getElementById("save");

    button.addEventListener("click", function () {

        clickCount++;

        console.log(`Clicked ${clickCount} times`);

    });

}

setupButton();
Enter fullscreen mode Exit fullscreen mode

Every click increases the counter.

Why?

Because the callback remembers clickCount.

Even after setupButton() has already finished executing.

Memory looks like this:

Event Listener

↓

Closure

↓

clickCount
Enter fullscreen mode Exit fullscreen mode

Without closures, every click would start from zero.


Closures with setTimeout()

One of the easiest ways to see closures is with timers.

function delayedGreeting() {

    let name = "John";

    setTimeout(function () {

        console.log(`Hello ${name}`);

    }, 3000);

}

delayedGreeting();
Enter fullscreen mode Exit fullscreen mode

Output (after 3 seconds)

Hello John
Enter fullscreen mode Exit fullscreen mode

Question:

How does name still exist?

Because the callback function carries a closure.

Even though delayedGreeting() already finished, JavaScript keeps the variable alive.


Closures Inside Loops

One of the most famous interview questions.

Consider this code:

for (var i = 1; i <= 5; i++) {

    setTimeout(function () {

        console.log(i);

    }, 1000);

}
Enter fullscreen mode Exit fullscreen mode

Output

6
6
6
6
6
Enter fullscreen mode Exit fullscreen mode

Most beginners expect:

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

Why?

Because every callback shares the same variable i.

After the loop ends:

i = 6
Enter fullscreen mode Exit fullscreen mode

Every closure references that same variable.


Fix #1 — Using let

for (let i = 1; i <= 5; i++) {

    setTimeout(function () {

        console.log(i);

    }, 1000);

}
Enter fullscreen mode Exit fullscreen mode

Output

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

Each iteration creates a brand-new variable.

Each closure gets its own copy.


Fix #2 — Using a Closure

Before let existed, developers solved it like this:

for (var i = 1; i <= 5; i++) {

    (function (currentValue) {

        setTimeout(function () {

            console.log(currentValue);

        }, 1000);

    })(i);

}
Enter fullscreen mode Exit fullscreen mode

Each Immediately Invoked Function Expression (IIFE) creates a new closure.

Each callback remembers its own value.


Closures in Factory Functions

Factory functions create objects.

Closures allow every object to have private state.

function createUser(name) {

    return {

        greet() {

            console.log(`Hello ${name}`);

        }

    };

}

const john = createUser("John");

john.greet();
Enter fullscreen mode Exit fullscreen mode

Output

Hello John
Enter fullscreen mode Exit fullscreen mode

Notice that name is completely private.

No one can access it directly.

console.log(john.name);
Enter fullscreen mode Exit fullscreen mode

Output

undefined
Enter fullscreen mode Exit fullscreen mode

Only the closure can access it.


Building a Counter

One of the classic closure examples.

function createCounter() {

    let count = 0;

    return {

        increment() {

            count++;

            return count;

        },

        decrement() {

            count--;

            return count;

        },

        reset() {

            count = 0;

        }

    };

}

const counter = createCounter();

console.log(counter.increment());

console.log(counter.increment());

console.log(counter.decrement());

counter.reset();

console.log(counter.increment());
Enter fullscreen mode Exit fullscreen mode

Output

1

2

1

1
Enter fullscreen mode Exit fullscreen mode

Notice something important.

There is no way to modify count directly.

Everything must go through the provided methods.

This is true data encapsulation.


Closures and Private Variables

JavaScript didn't originally have private fields.

Developers used closures instead.

function createBankAccount(initialBalance) {

    let balance = initialBalance;

    return {

        deposit(amount) {

            balance += amount;

        },

        withdraw(amount) {

            if (amount <= balance) {

                balance -= amount;

            }

        },

        getBalance() {

            return balance;

        }

    };

}
Enter fullscreen mode Exit fullscreen mode

Usage

const account = createBankAccount(1000);

account.deposit(500);

console.log(account.getBalance());
Enter fullscreen mode Exit fullscreen mode

Output

1500
Enter fullscreen mode Exit fullscreen mode

Trying this:

console.log(account.balance);
Enter fullscreen mode Exit fullscreen mode

Output

undefined
Enter fullscreen mode Exit fullscreen mode

Balance is truly private.


Closures in Express Middleware

Express middleware relies heavily on closures.

function logger(prefix) {

    return function (req, res, next) {

        console.log(prefix, req.method, req.url);

        next();

    };

}

app.use(logger("[API]"));
Enter fullscreen mode Exit fullscreen mode

Here:

prefix
Enter fullscreen mode Exit fullscreen mode

is preserved by the closure.

Every request has access to it.


Closures in React

React Hooks are built around closures.

Example:

function Counter() {

    const [count, setCount] = useState(0);

    function increment() {

        setCount(count + 1);

    }

}
Enter fullscreen mode Exit fullscreen mode

The function remembers the variables from its render.

This explains why stale closures sometimes happen in React.

Example:

setTimeout(() => {

    console.log(count);

}, 5000);
Enter fullscreen mode Exit fullscreen mode

If count changes before five seconds pass...

The callback may still reference the old value.

Understanding closures makes React much easier to understand.


The Once Function

Imagine an expensive initialization process.

You only want it to run once.

function once(fn) {

    let hasRun = false;

    let result;

    return function (...args) {

        if (!hasRun) {

            result = fn(...args);

            hasRun = true;

        }

        return result;

    };

}
Enter fullscreen mode Exit fullscreen mode

Usage

const initialize = once(function () {

    console.log("Initializing...");

    return "Application Ready";

});

console.log(initialize());

console.log(initialize());

console.log(initialize());
Enter fullscreen mode Exit fullscreen mode

Output

Initializing...

Application Ready

Application Ready

Application Ready
Enter fullscreen mode Exit fullscreen mode

Initialization happened exactly one time.


Why Does Once Work?

Memory inside the closure

hasRun = false

result = undefined
Enter fullscreen mode Exit fullscreen mode

First call

↓

Run function

↓

Store result

↓

hasRun = true
Enter fullscreen mode Exit fullscreen mode

Next calls

Skip execution

↓

Return stored result
Enter fullscreen mode Exit fullscreen mode

No global variables.

Everything lives inside the closure.


Building a Memoize Function

Memoization avoids repeating expensive computations.

function memoize(fn) {

    const cache = {};

    return function (...args) {

        const key = JSON.stringify(args);

        if (cache[key]) {

            console.log("Cache Hit");

            return cache[key];

        }

        console.log("Calculating...");

        const result = fn(...args);

        cache[key] = result;

        return result;

    };

}
Enter fullscreen mode Exit fullscreen mode

Usage

const square = memoize(function (number) {

    return number * number;

});

console.log(square(5));

console.log(square(5));

console.log(square(10));

console.log(square(10));
Enter fullscreen mode Exit fullscreen mode

Output

Calculating...

25

Cache Hit

25

Calculating...

100

Cache Hit

100
Enter fullscreen mode Exit fullscreen mode

The cache survives because of the closure.

Without closures, memoization would be impossible.


Module Pattern

Before ES Modules existed, developers used closures for encapsulation.

const UserModule = (function () {

    let users = [];

    function addUser(name) {

        users.push(name);

    }

    function getUsers() {

        return [...users];

    }

    return {

        addUser,

        getUsers

    };

})();
Enter fullscreen mode Exit fullscreen mode

Usage

UserModule.addUser("John");

UserModule.addUser("Alice");

console.log(UserModule.getUsers());
Enter fullscreen mode Exit fullscreen mode

Output

["John", "Alice"]
Enter fullscreen mode Exit fullscreen mode

The internal array cannot be accessed directly.

Everything is hidden inside the closure.


Why Professional Developers Love Closures

Closures provide:

  • Data Privacy
  • Encapsulation
  • State Management
  • Function Factories
  • Memoization
  • Dependency Injection
  • Functional Programming Patterns
  • Cleaner APIs
  • Better Reusability

Many popular JavaScript libraries use closures internally.

If closures disappeared from JavaScript today, React, Vue, Express middleware, Redux utilities, and many modern patterns would become much harder—or impossible—to implement in their current form.

Top comments (0)