正在加载,请稍候…

JavaScript Closures Explained: How They Work and Why They Matter

Understand JavaScript closures with clear examples. Learn how closures capture scope, common use cases like data privacy and factory functions, and pitfalls to avoid.

JavaScript Closures Explained: How They Work and Why They Matter

Closures are one of the most-asked JavaScript interview topics — and one of the most misunderstood. Once you truly get them, they become a powerful tool you'll use every day.

What Is a Closure?

A closure is a function that remembers the variables from the scope where it was defined, even after that scope has finished executing.

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

// count is not accessible from outside!
console.log(count); // ReferenceError: count is not defined

The inner function "closes over" the count variable. Even after makeCounter() returns, the inner function still has access to count.

How Closures Work: Lexical Scope

JavaScript uses lexical scoping: a function's scope is determined by where it's written in the code, not where it's called.

const name = 'Global';

function outer() {
  const name = 'Outer';
  
  function inner() {
    // inner() looks up scope chain: inner → outer → global
    console.log(name); // 'Outer' (not 'Global')
  }
  
  inner();
}

outer();

The scope chain for inner() is:

  1. inner's own scope
  2. outer's scope ← finds name here
  3. Global scope

Common Use Cases

1. Data Privacy / Module Pattern

function createBankAccount(initialBalance) {
  let balance = initialBalance; // Private — can't be accessed directly
  
  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(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500

// Can't access balance directly!
console.log(account.balance); // undefined

2. Factory Functions

function createMultiplier(factor) {
  // factor is closed over
  return (number) => number * factor;
}

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

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

3. Memoization (Caching)

function memoize(fn) {
  const cache = new Map(); // Closed over by the returned function
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('Cache hit!');
      return cache.get(key);
    }
    
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  // Simulates expensive computation
  let result = 0;
  for (let i = 0; i < n * 1000000; i++) result += i;
  return result;
});

expensiveCalc(10); // Computes (slow)
expensiveCalc(10); // Cache hit! (instant)

4. Partial Application

function multiply(a, b) {
  return a * b;
}

function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

const multiplyByFive = partial(multiply, 5);
console.log(multiplyByFive(3));  // 15
console.log(multiplyByFive(10)); // 50

5. Event Handlers with State

function createButton(label) {
  let clickCount = 0;
  
  const button = document.createElement('button');
  button.textContent = label;
  
  button.addEventListener('click', function() {
    // This handler closes over clickCount
    clickCount++;
    console.log(`"${label}" clicked ${clickCount} times`);
  });
  
  return button;
}

const btn1 = createButton('Save');    // Has its own clickCount
const btn2 = createButton('Delete');  // Has its own clickCount

The Classic Loop Bug (and Fix)

// ❌ Bug: all handlers close over the same 'i'
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // Prints 3, 3, 3 — not 0, 1, 2!
  }, 100);
}
// By the time timeouts run, the loop has finished and i === 3

// ✅ Fix 1: use let (creates new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // Prints 0, 1, 2 ✓
  }, 100);
}

// ✅ Fix 2: IIFE to capture i
for (var i = 0; i < 3; i++) {
  (function(capturedI) {
    setTimeout(function() {
      console.log(capturedI); // Prints 0, 1, 2 ✓
    }, 100);
  })(i);
}

Closures in Modern JavaScript

// React hooks use closures heavily
function Counter() {
  const [count, setCount] = useState(0);
  
  // handleClick closes over count and setCount
  const handleClick = () => {
    setCount(count + 1); // uses closed-over count
  };
  
  return <button onClick={handleClick}>{count}</button>;
}

// Stale closure issue (classic React bug)
function BadCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      // ❌ Stale closure: count is always 0 from first render
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []); // Empty deps — closes over initial count
  
  return <div>{count}</div>;
}

// Fix: use functional update
function GoodCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      // ✅ Functional update: always uses latest count
      setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);
  
  return <div>{count}</div>;
}

Memory Considerations

// ⚠️ Closures keep outer variables alive
function createHeavyObject() {
  const largeData = new Array(1000000).fill('data'); // 1M elements
  
  return function() {
    // largeData can never be garbage collected while this function exists!
    return largeData[0];
  };
}

// ✅ Limit what's closed over
function createHeavyObjectFixed() {
  const largeData = new Array(1000000).fill('data');
  const firstItem = largeData[0]; // Only capture what you need
  
  return function() {
    return firstItem; // largeData can now be GC'd
  };
}

Summary

  • A closure is a function + its captured scope (lexical environment)
  • Closures enable data privacy, factory functions, and stateful callbacks
  • Every function in JavaScript is a closure (they all capture their surrounding scope)
  • Watch for stale closures in React hooks and async code
  • Memory: closures keep referenced variables alive — avoid capturing large objects unnecessarily

→ Obfuscate your JavaScript code with the String Obfuscator tool.