← JS Mastery | Module 3: Functions Closures & Lexical Environment
Module 3

Closures & Lexical Environment

⏱ 24 min read ● Intermediate 🆓 Free

What is a Closure?

A closure is a function that "remembers" the variables from its outer scope even after the outer function has returned. It's one of JavaScript's most powerful features — and one of the trickiest to fully grasp.

function makeCounter() {
  let count = 0;  // This variable is "closed over"
  
  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter());  // 1
console.log(counter());  // 2
console.log(counter());  // 3

// makeCounter() has returned, but count still exists!
// The inner function closes over (keeps a reference to) count

const counter2 = makeCounter();  // separate closure, separate count
console.log(counter2());  // 1 — independent counter

Practical Closure Patterns

// Factory function — creates customized functions
function multiplier(factor) {
  return (number) => number * factor;  // closes over 'factor'
}

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

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

// Private data via closure
function createBankAccount(initialBalance) {
  let balance = initialBalance;  // private — not accessible from outside!
  
  return {
    deposit(amount) {
      if (amount > 0) balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);               // 150
account.withdraw(30);              // 120
// console.log(account.balance);  // undefined — truly private!

// Memoization — cache expensive results
function memoize(fn) {
  const cache = {};
  return function(n) {
    if (n in cache) {
      console.log("From cache");
      return cache[n];
    }
    cache[n] = fn(n);
    return cache[n];
  };
}

const slowSquare = (n) => {
  // imagine this is slow...
  return n * n;
};
const fastSquare = memoize(slowSquare);
fastSquare(10);  // computed
fastSquare(10);  // "From cache"

The Classic Closure Gotcha

// Classic bug — var in loops
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3  — all see the SAME i (final value)

// Fix 1: use let (block-scoped, each iteration gets own i)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2  ✅

// Fix 2: IIFE to create separate scope per iteration (old way)
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// Prints: 0, 1, 2  ✅

// This is why let was invented — it solves this naturally

⚡ Key Takeaways

🎯 Practice Exercises

EXERCISE 1

Create a makeAdder(x) function that returns a function adding x to its argument. Use it to create add5 and add10 functions.

EXERCISE 2 — CHALLENGE

Implement a rate limiter using closure: rateLimit(fn, limit, window) that only allows fn to be called limit times per window milliseconds.

← Scope & Hoisting