Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

深入理解 JavaScript 閉包

閉包是 JavaScript 面試必問,但真正理解的人不多。這篇從詞法作用域出發,講清楚什麼是閉包,以及實際工程中的應用。

詞法作用域:閉包的基礎

JavaScript 使用詞法作用域(也叫靜態作用域):函數的作用域在函數定義時決定,而不是調用時

javascript
const x = 10;

function outer() {
  const y = 20;

  function inner() {
    const z = 30;
    console.log(x, y, z); // 10, 20, 30
    // inner 能訪問 outer 和全局的變量
    // 這由函數定義的位置決定
  }

  inner();
}

outer();

什麼是閉包

閉包 = 函數 + 其定義時的詞法環境

當內部函數被外部引用,導致外部函數的變量無法被垃圾回收時,就形成了閉包:

javascript
function makeCounter() {
  let count = 0; // 這個變量會被"關閉"在閉包裏

  return function () {
    count++;
    return count;
  };
}

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

// count 變量沒有被銷燬,因為 counter 函數還引用着它

經典閉包陷阱

javascript
// ❌ 經典問題:循環裏的閉包
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i); // 輸出 5 5 5 5 5(不是 0 1 2 3 4)
  }, i * 1000);
}

// 原因:var 沒有塊級作用域,所有回調函數引用的是同一個 i
// 循環結束時 i = 5,所有回調執行時 i 都是 5

解決方案一:let(推薦)

javascript
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i); // 0 1 2 3 4(每次循環 let 創建新的綁定)
  }, i * 1000);
}

解決方案二:立即執行函數(IIFE)創建新作用域

javascript
for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j); // 0 1 2 3 4
    }, j * 1000);
  })(i);
}

實際應用場景

模塊化(封裝私有狀態)

javascript
const bankAccount = (function () {
  let balance = 0; // 私有,外部無法直接訪問

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) {
        throw new Error("餘額不足");
      }
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
})();

bankAccount.deposit(1000); // 1000
bankAccount.withdraw(200); // 800
bankAccount.getBalance(); // 800
// bankAccount.balance  → undefined(無法直接訪問)

函數工廠

javascript
function multiply(multiplier) {
  return function (number) {
    return number * multiplier;
  };
}

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

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

記憶化(Memoization)

javascript
function memoize(fn) {
  const cache = new Map(); // 閉包保存緩存

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveFn = memoize(function (n) {
  // 模擬耗時計算
  let result = 0;
  for (let i = 0; i < n * 1000000; i++) result += i;
  return result;
});

expensiveFn(100); // 第一次:慢
expensiveFn(100); // 第二次:瞬間(緩存命中)

閉包與內存泄漏

閉包會阻止垃圾回收,使用不當導致內存泄漏:

javascript
function attachHandler() {
  const largeData = new Array(1000000).fill("data");

  document.getElementById("btn").addEventListener("click", function () {
    // 回調函數引用了 largeData,導致其無法被 GC
    console.log(largeData.length);
  });
}

解決: 使用完後手動取消引用或移除事件監聽器:

javascript
// Vue 組件裏
mounted() {
  this.handler = () => { ... }
  element.addEventListener('click', this.handler)
},
beforeDestroy() {
  element.removeEventListener('click', this.handler)  // 清理
}

小結

  • 閉包 = 函數 + 其定義時的詞法環境
  • 循環裏用 let 避免經典閉包陷阱
  • 閉包用於封裝私有狀態、函數工廠、記憶化
  • 注意不必要的閉包持有大量數據,可能導致內存泄漏

MIT Licensed