JavaScript7 min read

JavaScript Closures Explained Simply

Understand JavaScript closures with clear examples. Learn how closures work, why they matter, and how to use them in real code patterns like debounce and private variables.

javascriptclosuresscopefunctions

JavaScript Closures Explained Simply

Closures are one of the most important concepts in JavaScript. They sound intimidating, but they're actually something you're already using — you just might not know it yet.

What is a Closure?

A closure is when a function "remembers" variables from its outer scope, even after that outer function has finished executing.

function createGreeter(greeting) {
  return function(name) {
    console.log(`${greeting}, ${name}!`);
  };
}

const sayHello = createGreeter("Hello");
sayHello("Priya");   // "Hello, Priya!"
sayHello("Rahul");   // "Hello, Rahul!"

createGreeter has already returned. It's done. But the inner function still has access to greeting. That's a closure.

How Closures Work

JavaScript has lexical scoping — a function looks up variables based on where it was defined, not where it's called.

When a function is created, it captures a reference to its surrounding scope. This captured scope stays alive as long as the function exists.

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

const myCounter = counter();
myCounter.increment();
myCounter.increment();
console.log(myCounter.getCount()); // 2

The variable count lives inside the closure. Nothing outside can access it directly — it's effectively private.

Why Closures Matter

1. Data Privacy

Closures let you create private variables in JavaScript — something the language doesn't natively support (without classes):

function createBankAccount(initialBalance) {
  let balance = initialBalance;
  
  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(1000);
account.deposit(500);     // 1500
account.withdraw(200);    // 1300
// account.balance — undefined! Can't access directly

2. Function Factories

Create specialized functions from a general template:

function multiply(factor) {
  return function(number) {
    return number * factor;
  };
}

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

double(5);  // 10
triple(5);  // 15

3. Callbacks and Event Handlers

Every callback that uses a variable from its enclosing scope is a closure:

function setupButton(buttonId, message) {
  const button = document.getElementById(buttonId);
  button.addEventListener("click", function() {
    alert(message); // 'message' is captured from outer scope
  });
}

setupButton("btn1", "Welcome!");
setupButton("btn2", "Goodbye!");

The Classic Closure Trap

This trips up every JavaScript developer at least once:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Prints: 3, 3, 3 (not 0, 1, 2!)

Why? var is function-scoped, not block-scoped. By the time the timeouts fire, i is already 3.

Fix with let:

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Prints: 0, 1, 2 ✓

let creates a new binding for each iteration — each callback gets its own i.

Fix with an IIFE (pre-ES6):

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

Real-World Closure Patterns

Debounce

function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

timeoutId is captured in the closure — it persists across calls.

Memoization

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key] !== undefined) return cache[key];
    cache[key] = fn.apply(this, args);
    return cache[key];
  };
}

The cache object lives in the closure — it remembers previous results.

Once Function

function once(fn) {
  let called = false;
  let result;
  return function(...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

Closures and Memory

Closures keep their outer scope alive, which means those variables stay in memory. This usually isn't a problem, but in rare cases (like storing closures in long-lived data structures), it can cause memory leaks.

Tip: If you're done with a closure, set the reference to null so the garbage collector can clean up.

Key Takeaways

  1. A closure is a function that remembers its outer scope
  2. Closures enable private variables, function factories, and callbacks
  3. Use let instead of var in loops to avoid the classic trap
  4. Debounce, throttle, and memoize all rely on closures
  5. Closures are everywhere in JavaScript — React hooks, event handlers, callbacks

🚀 Practice What You Learned

Apply these concepts with hands-on coding challenges: