Skip to main content

Command Palette

Search for a command to run...

Closures is easier than you think

Published
6 min readView as Markdown

Closures are often called one of the most confusing JavaScript topics.

The confusion does not come from syntax.
It comes from not seeing what happens in memory.

So in this blog:

  • We keep all the code

  • We explain step by step

  • You can visually see how JavaScript remembers variables in memory level


1️⃣ Inner & Outer Function – The Foundation

function outer() {
    let x = 10;

    function inner() {
        console.log(x);
    }

    inner();
}

outer();

What the code is doing

  • outer() creates variable x

  • inner() uses x

  • inner() runs while outer() is still executing

Visual: Scope without Closure

Key takeaways from this section?

  • Inner functions can access outer variables

  • Memory is destroyed after outer() finishes

  • This is NOT a closure yet


2️⃣ Making It a Closure (Returning the Inner Function)

Now we change only one thing.

function outer() {
    let x = 10;

    return function inner() {
        console.log(x);
    };
}

const func = outer();
func();

What changed?

  • inner() is returned

  • outer() finishes execution

  • But x is still accessible

Visual: Closure Retaining Memory

Why this works

  • func still references inner

  • inner still references x

  • Garbage Collector cannot remove x

This is a closure which I was talking about since now


3️⃣ Closure with State

function outerCount() {
    let count = 0;

    return function innerCount() {
        count++;
        console.log(count);
    };
}

const retVal = outerCount();

retVal(); // 1
retVal(); // 2
retVal(); // 3

What to notice

  • outerCount() runs once

  • count is created once

  • Value is remembered

Visual: State Preserved Across Calls

Key insight

Closures do not reset variables
They remember the last value [Important lession]


4️⃣ Real-World Closure – Bank Account (Encapsulation)

function createBankAccount(initialBalance) {
    let balance = initialBalance;

    return {
        deposit: (amount) => {
            balance = balance + amount;
            console.log("Deposited ", amount, " Current Balance ", balance);
        },

        withdraw: (amount) => {
            if (amount > balance) {
                console.warn("Insifficient Fund");
            } else {
                balance = balance - amount;
                console.log("Withdrawn ", amount, " Current Balance ", balance);
            }
        },

        checkBalance: () => console.log("Current Balance", balance),
    };
}

How This Maps EXACTLY to my Code

Code Execution

const BipulAccount = createBankAccount(100);

BipulAccount.deposit(300);   // 400
BipulAccount.withdraw(50);   // 350
BipulAccount.checkBalance(); // 350

What the diagram shows

  1. deposit(300)

    • balance: 100 → 400
  2. withdraw(50)

    • balance: 400 → 350
  3. checkBalance()

    • reads current value: 350

All three functions:

  • Share the same closure memory

  • Do not create new balance

  • Only read/update existing balance

Visual: Encapsulation Using Closure


5️⃣ Closures Can Cause Memory Leaks (Big Data) so where not to use closure

function dealingWithBigData() {
    let bigData = new Array(10000000).fill("*")

    return function() {
        console.log(bigData[3])
    }
}

const variable12 = dealingWithBigData();
variable12();

What Learners Usually Miss (Core Problem)

Most people think:

“I’m only using bigData[3], so the memory should be small.”

Which is absolutely Wrong

What actually happens:

  • Closure keeps a reference to the entire bigData array

  • Garbage Collector cannot free it

  • Memory stays allocated as long as the closure exists

This must be seen, not just read.

Correct Execution Flow (Step by Step)

Step 1️⃣ — Outer function executes

const variable12 = dealingWithBigData();
  • bigData array is created (10,000,000 items)

  • Inner function is returned

  • Outer function finishes execution

Normally memory would be cleaned…
but NOT here.


Step 2️⃣ — Closure is created

  • variable12 now holds the inner function

  • Inner function references bigData

  • Entire array stays in memory


Step 3️⃣ — Inner function executes

variable12();
  • Only bigData[3] is accessed

  • But whole array is still retained

Memory Leak via Closure

So, Be careful when closures hold large objects.

One-Line Takeaway for Readers

Closures don’t leak memory — long-lived references to large data do.


6️⃣ Timer Example – State Across Calls

function timer() {
    let secs = 0;

    return function() {
        secs++;
        console.log("elaspsed seconds ", secs)
    }
}

A Short Story from the JavaScript Engine’s Point of View

Hey again. JavaScript Engine here.
Let me tell you what happens when you run this timer code.


🟢 Scene 1: Timer Is Created

You write:

const timerInstance = timer();

I say:

“Alright, let me execute timer().”

Inside the function, I see:

let secs = 0;

So I do this:

“I’ll create a variable called secs and set it to 0.”

Then I see:

return function() {
    secs++;
    console.log("elapsed seconds ", secs);
}

I pause and think:

“This function uses secs.”

That’s my trigger.

So internally, I create this relationship:

                              timerInstance → inner function → secs

Then I finish executing timer().

Scene 3: First Call

You write:

timerInstance();

I execute:

secs++; // 0 → 1

And print:

elapsed seconds 1

I do not recreate secs.
I just update it.


🟢 Scene 4: Second & Third Calls

You call it again:

timerInstance();
timerInstance();

From my side, this is what happens:

secs = 1 → 2 → 3

What I think:

  • “Same function. Same memory. Just update the value.”

What You Should Learn from This

  • I don’t restart secs because timer() never runs again.

  • I reuse the same closure memory every time when i call the function.

One-Line Rule

  • Closures allow variables to live beyond their function’s execution and evolve over time.

7️⃣ Closures in Event Handlers

function setupButton() {
    let clickCount = 0;

    document.getElementById("myButton").addEventListener("click", function() {
        clickCount++;
        console.log(`Button clicked ${clickCount} times`);
    });
}

A Short Story from the JavaScript Engine’s Point of View

Still me. JavaScript Engine.
Now let’s talk about buttons and clicks.

Scene 1: Setup Phase

You write:

setupButton();

I execute setupButton() and see:

let clickCount = 0;

I say:

“Okay, click count starts at zero.”

Then I see:

document.getElementById("myButton")
  .addEventListener("click", function() {
      clickCount++;
      console.log(`Button clicked ${clickCount} times`);
  });

I immediately notice something important:

“This event handler function uses clickCount.”

So I do this internally:

event handler → clickCount

Then setupButton() finishes execution.

Scene 2: setupButton() Is Gone — But clickCount Is Not

Normally I would clean everything up.

But I stop myself:

“Wait. The event handler is still registered.”

That means:

  • The function still exists

  • It still references clickCount

So I keep clickCount alive.

This is another closure.

Scene 3: First Click

User clicks the button.

I execute the handler:

clickCount++; // 0 → 1

Console shows:

Button clicked 1 times

Scene 4: More Clicks

Each click triggers the same function.

From my perspective:

clickCount = 1234 → ...

I think:

“Same handler. Same memory. Just increment.”

I never reset clickCount, because:

  • setupButton() does not run again

  • The handler closure is still alive

Important Detail Most People Miss

The event handler:

  • Is not re-created per click

  • It is created once

  • And reused forever (until removed)

That’s why the count keeps increasing.


Final Mental Picture

If a function still has access to variables even after its outer function is gone — that function is a closure.

Closures are:

  • Predictable

  • Powerful

  • Everywhere in JavaScript