Welcome to DRIXO — Your Coding Journey Starts Here
DRIXO Code • Learn • Build

JavaScript Closures Explained with Examples

January 11, 2026 8 min read 0 Comments
JavaScript Closures Explained with Examples
JavaScript

JavaScript Closures Explained with Examples

DRIXO

Code · Learn · Build

If you've ever struggled with javascript closures, you're not alone. In this hands-on tutorial, I'll walk you through everything from the basics to real-world applications — with code you can actually use in your projects.

What Are Closures?

A closure is a function that remembers the variables from its outer scope, even after that outer function has finished executing. It's one of JavaScript's most powerful features.

Here's the simplest possible closure:

function createGreeter(name) {
  // 'name' is captured by the inner function
  return function() {
    console.log(`Hello, ${name}!`);
  };
}

const greetAlice = createGreeter('Alice');
const greetBob = createGreeter('Bob');

greetAlice(); // "Hello, Alice!"
greetBob();   // "Hello, Bob!"
// Each function "remembers" its own 'name' value

The inner function closes over the variable name. Even though createGreeter has already returned, the inner function still has access to name.

How Closures Work Under the Hood

When JavaScript creates a function, it also creates a lexical environment — a record of all variables in scope at that point. The closure holds a reference to this environment.

// Step-by-step execution:
function outer() {
  let count = 0;  // This lives in outer's lexical environment

  function inner() {
    count++;       // inner has a reference to outer's environment
    return count;
  }

  return inner;
}

const counter = outer();
// outer() is done, but its lexical environment survives
// because 'inner' still references it

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 'count' persists between calls!
Pro Tip: Closures don't copy the variable — they reference it. If two closures share the same outer scope, they share the same variable.

Practical Closure Patterns

Here are the most useful closure patterns you'll encounter in real projects:

Pattern 1: Private Variables (Module Pattern)

const bankAccount = (function() {
  let balance = 0;  // Private — can't be accessed directly

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error('Amount must be positive');
      balance += amount;
      return `Deposited $${amount}. Balance: $${balance}`;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error('Insufficient funds');
      balance -= amount;
      return `Withdrew $${amount}. Balance: $${balance}`;
    },
    getBalance() {
      return `$${balance}`;
    }
  };
})();

console.log(bankAccount.deposit(100));    // "Deposited $100. Balance: $100"
console.log(bankAccount.withdraw(30));    // "Withdrew $30. Balance: $70"
console.log(bankAccount.getBalance());    // "$70"
// bankAccount.balance → undefined (it's private!)

Pattern 2: Function Factory

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

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

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

// Useful for creating configurable functions
const addTax = createMultiplier(1.08);  // 8% tax
console.log(addTax(100));  // 108

Pattern 3: Event Handler with State

function createClickCounter(buttonId) {
  let clicks = 0;

  const button = document.getElementById(buttonId);
  button.addEventListener('click', function() {
    clicks++;
    button.textContent = `Clicked ${clicks} times`;

    if (clicks >= 10) {
      button.disabled = true;
      button.textContent = 'Max clicks reached!';
    }
  });
}

// Each button gets its own independent counter
createClickCounter('btn-1');
createClickCounter('btn-2');

Common Use Cases

Closures appear everywhere in JavaScript. Here are real-world scenarios:

Debounce Function

function debounce(func, delay) {
  let timeoutId;  // Closed over by the returned function

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

// Usage: Only trigger search after user stops typing for 300ms
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function(e) {
  console.log('Searching for:', e.target.value);
  // Make API call here
}, 300));

Memoization / Caching

function memoize(fn) {
  const cache = {};  // Closure keeps the cache alive

  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key] !== undefined) {
      console.log('Cache hit!');
      return cache[key];
    }
    console.log('Computing...');
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const expensiveCalc = memoize(function(n) {
  // Simulate expensive operation
  let result = 0;
  for (let i = 0; i < n * 1000000; i++) result += i;
  return result;
});

console.log(expensiveCalc(100));  // "Computing..." → result
console.log(expensiveCalc(100));  // "Cache hit!" → same result instantly

Closures and Memory

Closures keep variables alive in memory. This is usually fine, but can cause issues in loops:

The Classic Loop Problem

// BUG: All callbacks print 5
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);  // Always 5! var is function-scoped
  }, i * 100);
}

// FIX 1: Use let (block-scoped)
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);  // 0, 1, 2, 3, 4 ✓
  }, i * 100);
}

// FIX 2: Use IIFE to create new scope
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);  // 0, 1, 2, 3, 4 ✓
    }, j * 100);
  })(i);
}
Warning: If you capture large objects in closures and never release them, you'll have memory leaks. Set references to null when you're done.

Practice Exercises

Exercise 1: Create a Counter

Build a makeCounter() that returns an object with increment(), decrement(), and getValue():

function makeCounter(initial = 0) {
  let count = initial;
  return {
    increment() { return ++count; },
    decrement() { return --count; },
    getValue() { return count; },
    reset() { count = initial; return count; }
  };
}

const c = makeCounter(10);
console.log(c.increment()); // 11
console.log(c.increment()); // 12
console.log(c.decrement()); // 11
console.log(c.reset());     // 10

Exercise 2: Rate Limiter

function createRateLimiter(maxCalls, perSeconds) {
  const timestamps = [];

  return function(...args) {
    const now = Date.now();
    // Remove timestamps older than the window
    while (timestamps.length && timestamps[0] < now - perSeconds * 1000) {
      timestamps.shift();
    }

    if (timestamps.length >= maxCalls) {
      console.log('Rate limit exceeded! Try again later.');
      return null;
    }

    timestamps.push(now);
    console.log(`Call allowed (${timestamps.length}/${maxCalls})`);
    return true;
  };
}

const limiter = createRateLimiter(3, 10); // 3 calls per 10 seconds
limiter(); // "Call allowed (1/3)"
limiter(); // "Call allowed (2/3)"
limiter(); // "Call allowed (3/3)"
limiter(); // "Rate limit exceeded!"
AM
Arjun Mehta
Full-Stack Developer & Technical Writer at DRIXO

Full-stack developer with 5+ years of experience in Python and JavaScript. I love breaking down complex concepts into simple, practical tutorials. When I'm not coding, you'll find me contributing to open-source projects.

Comments