DEV Community

Cover image for 📜 JavaScript Deep Dive #1: Mastering Closures Like a Pro
Ahmed Bouchefra
Ahmed Bouchefra

Posted on

📜 JavaScript Deep Dive #1: Mastering Closures Like a Pro

Ever wondered how some JavaScript functions seem to “remember” variables even after they’ve finished running? That’s closures at work — one of JavaScript’s most powerful (and confusing) features.

In this guide, you’ll learn:

🔹 How closures preserve data in functions.

🔹 Why they’re essential for state management and encapsulation.

🔹 Common pitfalls (and how to avoid them).

🔹 How closures power React Hooks, event handlers, and async code.

🔹 How they compare to similar features in Python, Java, and C++.

By the end, you’ll not only understand closures — you’ll master them with real-world examples, debugging tricks, and challenges. Ready to level up? 🚀

“A fool thinks himself to be wise, but a wise man knows himself to be a fool.” — William Shakespeare

Closures might seem simple, but they can confuse even experienced developers. Let’s break them down properly.

🧠 Introduction: What Are Closures and Why Do They Matter?

Imagine you have a secret vault, and only a special key can open it. This key isn’t available to the world — it’s kept safe inside the vault.

Closures work the same way: an inner function keeps a reference to the variables of its outer function, even after the outer function has finished executing.

🔹 Why Are Closures Useful?

Data Privacy (Preventing Global Pollution)

State Persistence (Keeping Values Even After Execution)

Function Factories (Returning Customized Functions)

Event Listeners, Async Operations, and Module Patterns

Real-World Analogy: The Secret Vault 🔐1️⃣ The vault is created → A function is declared.

2️⃣
A key is stored inside the vault → A variable is kept inside the function.

3️⃣
Only a specific person can access it → An inner function has access to the variable, but the outside world doesn’t.

💻 Example: Understanding Closures Step by Step

🔹 Without Closures: Variables Don’t Persist

function counter() {  
  let count = 0;  
  count++;  
  return count;  
}  

console.log(counter()); // 1  
console.log(counter()); // 1 (Resets every time)
Enter fullscreen mode Exit fullscreen mode

🔴 Problem: Every time counter() is called, count is reset.

✅ Closures: Keeping Values Across Calls

function createCounter() {  
  let count = 0; // Private variable  
  return function () {  
    count++;   
    return count;  
  };  
}  
const myCounter = createCounter();  
console.log(myCounter()); // 1  
console.log(myCounter()); // 2  
console.log(myCounter()); // 3

Enter fullscreen mode Exit fullscreen mode

Why is this better?

  • Data Persistence: count retains its value across calls.
  • Encapsulation: count is protected; no direct access.
  • Reusability: Multiple independent counters can be created.

⚠️ Beginner Pitfalls & How to Overcome Them

🔴 1. Expecting closures to copy variables instead of reference them

Issue: Many beginners think closures store a copy of the variable at the time of function creation. Instead, they keep a reference to the original variable.

Example: Unexpected **setTimeout** behavior

for (var i = 1; i <= 3; i++) {  
  setTimeout(() => console.log(i), 1000);  
}  
// Logs: 4, 4, 4 (instead of 1, 2, 3)

Enter fullscreen mode Exit fullscreen mode

Fix: Use let to create a block-scoped variable.

for (let i = 1; i <= 3; i++) {  
  setTimeout(() => console.log(i), 1000);  
}  
// Correctly logs: 1, 2, 3

Enter fullscreen mode Exit fullscreen mode

🔎 Think Like a Compiler: How Closures Work Internally

Closures are possible because of lexical scoping:

  • When a function is created, it remembers the scope where it was declared.
  • Even after the outer function finishes execution, its variables are not destroyed if they are referenced inside a closure.

Example Visualization:


function outer() {   
  let a = 10;  
  return function inner() { console.log(a); }  
}  
const fn = outer();  
fn(); // Logs 10 (Even though `outer()` has finished)

Enter fullscreen mode Exit fullscreen mode

📌 Memory is only freed when there are no references to the closure.

⚡ Closures in the Wild: Real-World Applications

🔹 1. Avoiding Global Variables (Data Privacy)

function bankAccount(initialBalance) {  
  let balance = initialBalance;  
  return {  
    deposit(amount) {  
      balance += amount;  
      console.log(`New Balance: ${balance}`);  
    },  
    withdraw(amount) {  
      if (amount > balance) {  
        console.log("Insufficient funds!");  
      } else {  
        balance -= amount;  
        console.log(`New Balance: ${balance}`);  
      }  
    },  
    getBalance() {  
      return balance;  
    }  
  };  
}  

const myAccount = bankAccount(100);  
myAccount.deposit(50);  // New Balance: 150  
myAccount.withdraw(30); // New Balance: 120  
console.log(myAccount.getBalance()); // 120

Enter fullscreen mode Exit fullscreen mode

Here, **balance** is private and only accessible through methods!

🔹 2. Closures in React’s **useState** (Modern Web Development)


function Counter() {  
  const [count, setCount] = React.useState(0);

 return (  
    <button onClick={() => setCount(count + 1)}>  
      Count: {count}  
    </button>  
  );  
}

Enter fullscreen mode Exit fullscreen mode

Closures allow **setCount** to remember the previous state.

🔄 Closures in Other Languages (If You Come From…)

🔹 Python (Similar: Inner Functions & **nonlocal**)

Closures in Python work similarly using inner functions and **nonlocal**:

def counter():  
    count = 0  
    def increment():  
        nonlocal count  
        count += 1  
        return count  
    return increment  

my_counter = counter()  
print(my_counter())  # 1  
print(my_counter())  # 2

Enter fullscreen mode Exit fullscreen mode

🔹 Java (No Direct Equivalent, but Similar to Anonymous Classes)

Java doesn’t have true closures, but anonymous classes with final variables behave similarly.

🔹 C++ (Lambdas Capture State)

std::function<int()> createCounter() {  
    int count = 0;  
    return [=]() mutable { return ++count; };  
}

Enter fullscreen mode Exit fullscreen mode

Closures in C++ are captured in lambdas, just like JavaScript functions.

🛠️ If You Understand This, Try… (Challenge Section)

🔹 Challenge: Implement a function that only runs once using closures.

function once(fn) {  
  let ran = false;  
  return function (...args) {  
    if (!ran) {  
      ran = true;  
      return fn(...args);  
    }  
  };  
}  

const init = once(() => console.log("This runs only once!"));  
init(); // Runs ✅  
init(); // Doesn't run ❌

Enter fullscreen mode Exit fullscreen mode

Used in JavaScript libraries like Lodash!

📖 Learn More: Best Books & Resources on Closures

📚 Free Resources

💰 Paid Books for Deep Understanding

  • “You Don’t Know JS: Scope & Closures” by Kyle Simpson — The ultimate guide to closures and scope.
  • “JavaScript: The Good Parts” by Douglas Crockford — Covers closures as a fundamental JS feature.

🎯 Conclusion: Why Closures Are Essential

Closures allow functions to “remember” data even after execution. They help in data privacy, function factories, and efficient state management.

Next time you use setTimeout, event listeners, or create custom counters, remember — you’re leveraging the power of closures.

What JavaScript concept should we break down next? 🚀

Top comments (1)

Collapse
 
wizard798 profile image
Wizard

Very in depth knowledge of this, thanks for this amazing article, I already knew what closures are and how it works but this even enhanced my knowledge about closures