JavaScript 閉包與作用域簡單解析

Frontend2026-09-19TryQuickToolBox

你正在寫一個 JavaScript 函式,它會回傳另一個函式。內部函式使用了外部函式的變數,但當你稍後呼叫它時,外部函式早已執行完畢。然而那個變數仍然存在。這是怎麼辦到的?這就是閉包與作用域的魔法。許多開發者對這個概念感到困惑,因而產生像是意外的變數值或記憶體洩漏等錯誤。在本文中,我們將用簡單的方式並搭配實用範例,拆解 JavaScript 閉包與作用域。

JavaScript 中的作用域是什麼?

作用域決定變數在程式碼中哪些地方可以被存取。JavaScript 主要有三種作用域:

這裡有個快速範例:

let globalVar = 'I am global';

function myFunc() {
  let functionVar = 'I am in function scope';
  if (true) {
    let blockVar = 'I am in block scope';
    console.log(globalVar); // accessible
    console.log(functionVar); // accessible
    console.log(blockVar); // accessible
  }
  console.log(blockVar); // ReferenceError: blockVar is not defined
}

myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined

理解作用域至關重要,因為它能防止變數衝突並協助管理記憶體。

詞法作用域:閉包的基礎

JavaScript 使用詞法作用域,意思是變數的作用域由其原始碼中的位置決定。內部函式可以存取其外部(封閉)作用域中的變數。這也稱為靜態作用域。

考慮以下範例:

function outer() {
  let outerVar = 'outer';
  function inner() {
    console.log(outerVar); // 'outer'
  }
  inner();
}
outer();

inner 函式可以存取 outerVar,因為它在詞法上位於 outer 內部。這不是動態的;它不取決於函式如何被呼叫。

什麼是閉包?

閉包是一個函式,即使該函式在其詞法作用域之外執行,它仍記得自己的詞法作用域。換句話說,閉包讓你從內部函式存取外部函式的作用域。閉包會在每次建立函式時建立,也就是在函式建立時。

範例:

function makeCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

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

在這裡,makeCounter 回傳一個會遞增 count 的內部函式。即使 makeCounter 已經執行完畢,回傳的函式仍然可以存取 count。這就是閉包。

閉包在底層如何運作

當函式被建立時,它會儲存對其詞法環境(作用域鏈)的參照。當函式被呼叫時,它會使用該環境來解析變數。只要閉包存在,該環境就會一直保持存活。

閉包的實務用途

閉包不只是理論;它們在 JavaScript 中無處不在。

1. 資料隱私(模組模式)

在 ES6 模組之前,閉包被用來建立私有變數:

const counterModule = (function() {
  let count = 0;
  return {
    increment: function() { count++; },
    getCount: function() { return count; }
  };
})();

counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined

在這裡,count 是私有的,只能透過回傳的方法存取。

2. 事件處理常式與回呼

閉包常用於事件處理常式中,以存取外部作用域的變數:

function setupButton() {
  let clickCount = 0;
  document.getElementById('myButton').addEventListener('click', function() {
    clickCount++;
    console.log(`Button clicked ${clickCount} times`);
  });
}

事件處理常式閉包會記住 clickCount。

3. 函式工廠

你可以建立帶有預設參數的函式:

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

const double = multiplyBy(2);
console.log(double(5)); // 10

常見陷阱與如何避免

如果不理解閉包,可能會導致意外行為。

迴圈變數捕獲

在迴圈中使用 var 會建立單一共用綁定:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 3, 3, 3
  }, 100);
}

所有回呼共用同一個 i。要修正,請使用 let(區塊作用域)或 IIFE:

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 0, 1, 2
  }, 100);
}

記憶體洩漏

閉包會讓變數保持存活。如果閉包被附加到長生命週期的物件(例如 DOM 元素),它可能會阻止垃圾回收。當不再需要時,請務必移除事件監聽器。

作用域鏈與變數解析

當你存取變數時,JavaScript 會沿著作用域鏈查找:先找目前作用域,再找外部作用域,一直到全域作用域。如果找不到,就會拋出 ReferenceError。

範例:

let a = 'global';
function outer() {
  let b = 'outer';
  function inner() {
    let c = 'inner';
    console.log(a, b, c); // all accessible
  }
  inner();
}
outer();

inner 的作用域鏈是:inner -> outer -> global。

比較:var、let 和 const

關鍵字作用域提升可重新賦值
var函式是(初始化為 undefined)是
let區塊是(但未初始化,TDZ)是
const區塊是(但未初始化,TDZ)否

理解這些差異是避免作用域相關錯誤的關鍵。

常見問題

JavaScript 中的閉包是什麼?

閉包是一個函式,即使在其詞法作用域之外執行,仍保留對該作用域的存取權。它讓函式能夠「記住」定義它的位置中的變數。

為什麼閉包很重要?

閉包能實現資料隱私、函式工廠,以及維護狀態的事件處理常式。它們是許多 JavaScript 模式的基礎。

如何避免閉包相關的錯誤?

使用 let 和 const 來達到區塊作用域,注意迴圈,並清理事件監聽器以防止記憶體洩漏。

準備好測試你的理解了?試試我們的 JSON Formatter 來實驗程式碼片段,並即時觀察閉包如何運作。