JavaScript 深度
閉包 × 作用域

閉包與作用域
JS 最核心的底層機制

搞懂作用域鏈、var / let / const 的差異、閉包的形成原理,
以及那些最容易在面試和實際開發中踩到的坑

J

Joseph Chen

2026·JavaScript 深度系列·5 min read·1.2k views
作用域
閉包
var/let/const

一、什麼是作用域(Scope)?

作用域(Scope)決定了「一個變數在哪裡可以被讀取或修改」。 JavaScript 有三種作用域層級,理解它們是搞懂閉包的第一步。

全域作用域

Global Scope

在任何函式或區塊之外宣告的變數,程式碼的任何地方都能存取。

const name = "Joseph";
// 整份程式碼都可以用 name

函式作用域

Function Scope

在函式內部宣告的變數,只有函式內部可以存取,外部無法讀取。

function greet() {
  const msg = "Hi";
}
// msg 在這裡是 undefined

區塊作用域

Block Scope

用 {} 包起來的區塊內,let / const 宣告的變數只在該區塊內有效。

if (true) {
  let x = 1;
}
// x 在這裡是 undefined

二、var / let / const 與作用域的本質差異

這三個關鍵字的差異不只是「能不能重新賦值」,最根本的差別在於作用域的大小提升(Hoisting)的行為

特性varletconst
作用域函式作用域區塊作用域區塊作用域
可重新賦值✅ 可以✅ 可以❌ 不行
可重新宣告✅ 可以❌ 不行❌ 不行
Hoisting✅(值為 undefined)✅(TDZ,存取報錯)✅(TDZ,存取報錯)
全域物件屬性✅ window.xxx❌ 不會掛上❌ 不會掛上
var-function-scope.js — var 的陷阱
// var 是函式作用域,不是區塊作用域!
function example() {
  for (var i = 0; i < 3; i++) {
    // 以為 i 只在 for 迴圈裡,其實不是
  }
  console.log(i); // 3for 迴圈外仍可存取!
}

// let 是區塊作用域,這才是你預期的行為
function example2() {
  for (let j = 0; j < 3; j++) {
    // j 只存在 for 區塊內
  }
  console.log(j); // ❌ ReferenceError: j is not defined
}

Hoisting(提升)與暫時死區(TDZ)

Hoisting 是指 JS 引擎在執行程式碼之前,會先把所有宣告「提升」到作用域頂部。 但 letconst 雖然也會提升, 但在宣告那行之前讀取它們會進入暫時死區(Temporal Dead Zone),直接報錯。

hoisting-tdz.js
// var 的 hoisting:宣告被提升,但值是 undefined
console.log(a); // undefined(不報錯!)
var a = 5;
console.log(a); // 5

// let 的 TDZ:宣告被提升,但在初始化前存取就報錯
console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 10;

// 這就是為什麼「用 let/const 比 var 更安全」——
// 它們會幫你抓到「先用後宣告」這種邏輯錯誤

三、作用域鏈(Scope Chain)

當 JS 查找一個變數時,它會先在目前作用域找,找不到就往外層作用域找, 一層一層向上,直到全域作用域為止。這個查找鏈就叫做作用域鏈

scope-chain.js
const x = 'global';      // 全域作用域

function outer() {
  const x = 'outer';     // outer 的函式作用域

  function inner() {
    const x = 'inner';   // inner 的函式作用域
    console.log(x);      // 'inner'(在自己的作用域就找到了)
  }

  function inner2() {
    // inner2 自己沒有 x
    console.log(x);      // 'outer'(往外一層找到 outer 的 x)
  }

  inner();   // 'inner'
  inner2();  // 'outer'
}

outer();
console.log(x); // 'global'(全域的 x)
💡
詞法作用域(Lexical Scope):JS 的作用域是在寫程式碼的時候就決定的, 不是執行的時候決定的。也就是說,一個函式能存取哪些變數,看的是它被定義在哪裡, 而不是被呼叫在哪裡。這個特性是閉包能夠運作的根本原因。

四、閉包(Closure)深度解析

閉包是 JS 中最常被問到也最常被誤解的概念。簡單說,閉包就是:「一個函式,記得它被定義時所在的作用域,即使那個作用域已經執行完畢。」

closure-basic.js — 閉包的形成
function makeAdder(x) {
  // makeAdder 執行完後,x 理論上應該消失…
  return function(y) {
    return x + y;  // 但內層函式「記住」了 x!
  };
}

const add5 = makeAdder(5);  // makeAdder 已執行完畢
const add10 = makeAdder(10);

console.log(add5(3));   // 8   ← x 還在,等於 5 + 3
console.log(add10(3));  // 13  ← 這個 x 是 10,等於 10 + 3

// add5 和 add10 各自持有獨立的 x 值
// 它們是兩個不同的閉包

閉包形成的三個條件

01

有一個外層函式(提供作用域)

02

外層函式內部定義了一個內層函式

03

內層函式引用了外層函式的變數,且被回傳或傳遞到外部使用

閉包的實際應用場景

應用 1

私有變數(Private State)

JS 沒有原生的 private 關鍵字(class 的 # 是後來才加的), 閉包是在 ES6 之前實現私有狀態的標準做法。

private-state.js
function createBankAccount(initialBalance) {
  let balance = initialBalance;  // 外部無法直接存取 balance

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

const account = createBankAccount(1000);
account.deposit(500);     // 1500
account.withdraw(200);    // 1300
account.getBalance();     // 1300
account.balance;          // undefined ← 無法直接存取!
應用 2

函式工廠(Function Factory)

用閉包「預先固定」部分參數,產生一系列相關函式。這在 React 的事件處理中非常常見。

function-factory.js
// 通用的乘法工廠
const multiply = (factor) => (num) => num * factor;

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

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

// React 中的實際使用場景
// const handleClick = (id) => () => deleteItem(id);
// <button onClick={handleClick(item.id)}>刪除</button>
應用 3

快取(Memoization)

memoize.js
function memoize(fn) {
  const cache = {};  // 閉包記住這個快取物件

  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      console.log('從快取回傳');
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const slowSquare = (n) => {
  // 假設這是一個耗時計算
  return n * n;
};

const fastSquare = memoize(slowSquare);
fastSquare(10); // 計算,回傳 100,存入快取
fastSquare(10); // 從快取回傳 100,不重新計算
fastSquare(10); // 從快取回傳 100

五、最常見的閉包陷阱

陷阱 #1

var 在迴圈中建立閉包

這是最經典的面試題。用 var 的迴圈,所有閉包共享同一個變數。

❌ var — 全部印出 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 印出:3 3 3
// 因為 var i 是函式作用域,
// 三個閉包共享同一個 i,
// 執行時 i 已經是 3 了
✅ let — 各自獨立
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 印出:0 1 2
// let 是區塊作用域,
// 每次迭代都建立一個新的 i,
// 各閉包記住自己那份 i

這個問題的根本原因是 var 的函式作用域。用 let 替換 var,讓每次迭代都有獨立的區塊作用域,閉包就能各自記住正確的 i 值。

陷阱 #2

閉包造成的記憶體洩漏

閉包會讓外層函式的變數一直存在記憶體中,如果閉包持有大型資料且沒有釋放,會造成記憶體洩漏。

memory-leak.js
function createLeak() {
  const bigData = new Array(1000000).fill('data'); // 大型資料

  return function() {
    // 這個閉包只用到 bigData.length,
    // 但 bigData 整個陣列都被保留在記憶體中
    return bigData.length;
  };
}

// ✅ 改法:只保留需要的值
function createNoLeak() {
  const bigData = new Array(1000000).fill('data');
  const len = bigData.length; // 只記住需要的值

  // bigData 可以被垃圾回收了
  return function() {
    return len;
  };
}
⚠️
在 React 中,useCallback / useMemo 的 dependency array 如果漏掉了某個閉包引用的變數, 可能導致閉包「記住舊的值」而不更新,這是 stale closure(過時閉包)問題。
陷阱 #3

閉包不能「記住」this

閉包記住的是外層作用域的變數,但 this 不是變數,它是由呼叫方式決定的。 這是一個不少人混淆的地方。

this-closure.js
const obj = {
  name: 'Joseph',

  // ❌ 傳統函式:this 由呼叫方式決定
  greetTraditional: function() {
    setTimeout(function() {
      console.log(this.name); // undefined(this 是 window/undefined)
    }, 100);
  },

  // ✅ 箭頭函式:this 繼承定義時的外層 this
  greetArrow: function() {
    setTimeout(() => {
      console.log(this.name); // 'Joseph' ← 正確!
    }, 100);
  },
};

obj.greetTraditional(); // undefined
obj.greetArrow();       // 'Joseph'
💡
箭頭函式沒有自己的 this,它會繼承定義時外層作用域的 this。 這就是 React 事件處理器通常用箭頭函式的原因——可以正確讀取元件實例的 this

六、核心重點整理

作用域

決定變數在哪裡可以被讀取,分全域/函式/區塊三層

作用域鏈

找不到變數就往外層找,一直找到全域;基於詞法作用域(寫程式的位置決定)

var

函式作用域、會 Hoisting(值為 undefined)、可重複宣告 → 避免使用

let / const

區塊作用域、TDZ(宣告前存取報錯)、let 可重新賦值,const 不行

閉包

內層函式記住外層作用域變數引用,即使外層函式已執行完畢

var + 閉包迴圈

所有閉包共享同一個 var 變數 → 用 let 解決,各次迭代有獨立作用域

this ≠ 閉包

this 由呼叫方式決定,不被閉包記住 → 箭頭函式繼承外層 this