Munsif.
AboutExperienceProjectsAchievementsBlogsContact
HomeAboutExperienceProjectsAchievementsBlogsContact
Munsif.

Frontend Developer crafting scalable web applications with modern technologies and clean code practices.

Quick Links

  • About
  • Experience
  • Projects
  • Achievements

Connect

© 2026 Shaik Munsif. All rights reserved.

Built with Next.js & Tailwind

0%
Welcome back!Continue where you left off
Back to Blogs
JavaScript

JavaScript Closures Deep Dive: Mastering Lexical Scope & Memory

Master JavaScript closures from fundamentals to advanced patterns. Learn lexical scoping, avoid memory leaks, and leverage closures for data privacy, memoization, and functional programming.

Jan 20, 202622 min read
JavaScriptClosuresFunctional ProgrammingInterview PrepDeep Dive

Introduction

If you've been coding JavaScript for a while, you've probably used closures without even realizing it. Every time you write an event handler, create a factory function, or use setTimeout with variables from an outer scope—you're using closures.

But what exactly is a closure? How does it work under the hood? Why is it considered one of the most powerful (and misunderstood) features of JavaScript?

In this deep dive, we'll demystify closures completely. You'll learn what they are, how they work, when to use them, and how to avoid common pitfalls like memory leaks. By the end, you'll not only understand closures—you'll be able to leverage them like a senior engineer.


What is a Closure?

A closure is a function that has access to variables from its outer (enclosing) scope, even after that outer function has finished executing.

Let's break this down with a simple example:

function outerFunction() {
    const message = 'Hello from outer!';
    
    function innerFunction() {
        console.log(message); // Can access 'message' from outer scope
    }
    
    return innerFunction;
}

const myFunction = outerFunction();
myFunction(); // "Hello from outer!"

What just happened?

  1. outerFunction executed and finished
  2. Normally, the variable message would be garbage collected
  3. But innerFunction created a closure over message
  4. Even after outerFunction completed, message is still accessible
💜 important

[!IMPORTANT] A closure is created whenever a function "remembers" variables from its birth place (lexical scope), regardless of where it's executed later.


The Bank Vault Analogy

Think of a closure like a bank vault:

  • The Outer Function = The bank building
  • Variables in Outer Function = Money/valuables inside the vault
  • Inner Function (Closure) = Your personal key to the vault

When the bank closes for the day (outer function finishes), you still have your key. You can come back anytime and access what's inside the vault. Other people can't access it—only you have the key. That's exactly how closures preserve access to variables.


Lexical Scoping: The Foundation

To understand closures, you first need to understand lexical scoping.

JavaScript uses lexical (static) scoping, which means a function's scope is determined by where it's written in the code, not where it's called.

const globalVar = 'I am global';

function outer() {
    const outerVar = 'I am outer';
    
    function inner() {
        const innerVar = 'I am inner';
        console.log(globalVar); // ✅ Can access
        console.log(outerVar);  // ✅ Can access
        console.log(innerVar);  // ✅ Can access
    }
    
    inner();
}

outer();

The Scope Chain:

  1. inner() looks for variables in its own scope first
  2. If not found, it looks in the outer function's scope
  3. If still not found, it looks in the global scope
  4. If still not found → ReferenceError

This "looking up the chain" mechanism is what enables closures.


Real-World Example 1: Counter Function

One of the most common uses of closures is to create private variables.

function createCounter() {
    let count = 0; // Private variable
    
    return {
        increment() {
            count++;
            console.log(count);
        },
        decrement() {
            count--;
            console.log(count);
        },
        getCount() {
            return count;
        }
    };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
console.log(counter.getCount()); // 1

// ❌ Can't access 'count' directly
console.log(counter.count); // undefined

Why is this powerful?

  • count is private—it can't be accessed or modified directly
  • Only through the methods (increment, decrement, getCount) can you interact with it
  • This is data encapsulation in JavaScript, achieved through closures

Real-World Example 2: Event Handlers

Closures are everywhere in event handling:

function setupClickCounter() {
    let clickCount = 0;
    
    const button = document.querySelector('#myButton');
    
    button.addEventListener('click', function() {
        clickCount++;
        console.log(`Button clicked ${clickCount} times`);
    });
}

setupClickCounter();

Even though setupClickCounter() finishes executing immediately, the event handler still has access to clickCount because of closure. Every time you click the button, it increments the same clickCount variable.


Real-World Example 3: Factory Functions

Closures enable powerful factory patterns:

function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Each function (double, triple) carries its own multiplier value in its closure. They're independent instances with their own private state.


The Classic Interview Trap: Closures in Loops

This is the #1 interview question about closures:

❌ The Problem

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

// Output after 1 second:
// 3
// 3
// 3

Why does this happen?

  1. var is function-scoped, not block-scoped
  2. All three setTimeout callbacks share the same i variable
  3. By the time the callbacks execute (after 1 second), the loop has finished
  4. The value of i is now 3

✅ Solution 1: Use let (Block Scope)

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

// Output after 1 second:
// 0
// 1
// 2

Why does this work?

let creates a new binding for each iteration of the loop. Each setTimeout callback gets its own copy of i.

✅ Solution 2: IIFE (Immediately Invoked Function Expression)

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })(i);
}

// Output after 1 second:
// 0
// 1
// 2

Why does this work?

The IIFE creates a new scope for each iteration, capturing the current value of i as j.


Advanced Pattern: Memoization

Closures enable performance optimization through memoization (caching results):

function memoize(fn) {
    const cache = {}; // Closure variable
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (key in cache) {
            console.log('Returning cached result');
            return cache[key];
        }
        
        console.log('Computing result');
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

// Example: Expensive calculation
const slowSquare = (n) => {
    // Simulate expensive operation
    for (let i = 0; i < 1000000000; i++) {}
    return n * n;
};

const fastSquare = memoize(slowSquare);

console.log(fastSquare(5)); // "Computing result" → 25
console.log(fastSquare(5)); // "Returning cached result" → 25 (instant!)

The cache object lives in the closure, persisting between function calls.


Advanced Pattern: Module Pattern

Before ES6 modules, closures were the primary way to create modules:

const Calculator = (function() {
    // Private variables
    let result = 0;
    
    // Private function
    function log(operation, value) {
        console.log(`${operation}: ${value}`);
    }
    
    // Public API
    return {
        add(value) {
            result += value;
            log('Added', value);
            return this;
        },
        subtract(value) {
            result -= value;
            log('Subtracted', value);
            return this;
        },
        getResult() {
            return result;
        }
    };
})();

Calculator.add(10).add(5).subtract(3);
console.log(Calculator.getResult()); // 12

// ❌ Can't access private members
console.log(Calculator.result); // undefined
Calculator.log('test', 1);       // TypeError

This pattern creates a self-contained module with private and public members, all powered by closures.


Advanced Pattern: Currying

Currying transforms a function with multiple arguments into a sequence of functions each taking a single argument:

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            return function(...nextArgs) {
                return curried.apply(this, args.concat(nextArgs));
            };
        }
    };
}

// Example function
function sum(a, b, c) {
    return a + b + c;
}

const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3));       // 6
console.log(curriedSum(1, 2)(3));       // 6
console.log(curriedSum(1)(2, 3));       // 6

Each returned function closes over the accumulated arguments, demonstrating closures at work.


The Dark Side: Memory Leaks

Closures keep variables in memory. If not careful, this can cause memory leaks.

⚠️ Common Leak Pattern

function attachHandler() {
    const largeData = new Array(1000000).fill('data');
    
    document.querySelector('#button').addEventListener('click', function() {
        console.log('Button clicked');
        // This closure captures 'largeData' even though it's not used!
    });
}

attachHandler();

The Problem:

Even though largeData is never used in the callback, JavaScript keeps it in memory because the closure could access it. If you call attachHandler() multiple times, you'll accumulate large unused data in memory.

✅ Solution: Break the Reference

function attachHandler() {
    const largeData = new Array(1000000).fill('data');
    
    // Use the data if needed
    processData(largeData);
    
    // Create handler without referencing largeData
    document.querySelector('#button').addEventListener('click', function() {
        console.log('Button clicked');
    });
}

Or use the data and nullify the reference:

function attachHandler() {
    let largeData = new Array(1000000).fill('data');
    
    document.querySelector('#button').addEventListener('click', function() {
        console.log('Button clicked');
    });
    
    largeData = null; // Allow garbage collection
}

Closures vs Arrow Functions

Arrow functions handle closures the same as regular functions—they capture variables from their outer scope. However, they differ in this binding:

const obj = {
    name: 'Alice',
    regularFunction: function() {
        setTimeout(function() {
            console.log(this.name); // undefined (this = window/global)
        }, 1000);
    },
    arrowFunction: function() {
        setTimeout(() => {
            console.log(this.name); // "Alice" (this = obj)
        }, 1000);
    }
};

obj.regularFunction(); // undefined
obj.arrowFunction();   // "Alice"

Arrow functions close over this from their lexical scope, making them ideal for callbacks.


5 Interview Questions You MUST Know

1. What is a closure in JavaScript?

Answer: A closure is a function that retains access to variables from its outer (enclosing) scope, even after the outer function has finished executing. Closures are created whenever a function is defined inside another function.

2. Why do all three logs print "3" in this code?

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0);
}

Answer: Because var is function-scoped, not block-scoped. All three setTimeout callbacks share the same i variable. By the time they execute, the loop has finished and i is 3. Using let instead of var fixes this, as let creates a new binding for each iteration.

3. How can you use closures to create private variables?

Answer: Return an object with methods that access the private variables through closures:

function createPerson(name) {
    let age = 0; // Private
    
    return {
        getName() { return name; },
        getAge() { return age; },
        haveBirthday() { age++; }
    };
}

The variables name and age are only accessible through the returned methods, not directly.

4. What's the difference between a closure and a regular function?

Answer: All functions in JavaScript are closures—they all have access to their outer scope. The term "closure" specifically emphasizes when this behavior is significant: when a function accesses variables from an outer scope that has already completed execution.

5. Can closures cause memory leaks?

Answer: Yes. Closures keep outer variables in memory even if they're not used. If you create many closures that capture large data structures, it can cause memory issues. Modern JavaScript engines optimize this, but it's still important to be mindful of what variables closures capture, especially in long-running applications.


Conclusion

Closures are a fundamental pillar of JavaScript that enable:

  • Data privacy and encapsulation
  • Factory functions with persistent state
  • Higher-order functions and functional programming
  • Event handlers with access to outer scope
  • Performance optimizations like memoization

Understanding closures transforms you from someone who writes JavaScript to someone who truly understands JavaScript. They're not just a feature—they're the key to writing elegant, powerful, and maintainable code.

The next time you write a callback or return a function, remember: you're wielding one of JavaScript's most powerful tools. Use it wisely!

🧠 Test Your Knowledge

Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.

Written by

Shaik Munsif

Read more articles

Found this helpful? Share it with your network!

Question 1 of 10Easy
Score: 0/0

What is a closure in JavaScript?