iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
JavaScript

React 觀念架構:從js 基礎到Hook 底層邏輯 系列 第 3

DAY 3 閉包 (Closure) 與作用域鏈:理解 Hook State 歷史紀錄的基石

  • 分享至 

  • xImage
  •  

在昨天我們搞懂了 JS 的記憶體模型與不可變性。今天我們要第二個 JS 觀念:閉包 (Closure)。

一、 什麼是閉包 (Closure)?

簡單來說:「當一個內部函數被定義時,它會『封裝』並記住其外層作用域 (Scope) 的變數,即使外層函數已經執行完畢,這些變數依然會被保留在記憶體中。」

看一個 JS 範例:

function createCounter() {
  let count = 0; // 外層變數
  
  return function increment() {
    count++; // 內部函數存取外層變數
    console.log(count);
  };
}

const counter = createCounter(); // createCounter 執行完了
counter(); // 1
counter(); // 2 (count 沒有被回收仍然被記憶)

記憶體拆解:

createCounter() 執行完畢後,照理說裡面的 count 應該被銷毀(GC 垃圾回收)。

但因為回傳的 increment 函數引用了外層的 count,形成了一個「閉包包囊 (Closure Environment)」。

只要 counter 函數還存在,count 變數就會一直留在記憶體中。

二、 簡化版 React State 模擬:閉包的妙用

React 組件本質上只是一個函數,每次 Re-render 都是重新呼叫該函數。為什麼裏頭的變數不會被重置?

我們寫一個極簡 useState 模擬:

// 模擬 React 的全域/模組作用域
let _state;

function useState(initialValue) {
  // 如果是第一次呼叫,就用 initialValue,否則使用保留的 _state
  _state = _state === undefined ? initialValue : _state;

  function setState(newValue) {
    _state = newValue;
    render(); // 觸發重新渲染
  }

  return [_state, setState]; // setState 透過閉包記住了 _state
}

當組件呼叫 setState 時,setState 透過閉包存取並修改了外層的 _state 變數。這就是 React 能在多次渲染間「記錄歷史 State」的底層邏輯。

三、 React 中經典的「過時閉包 (Stale Closure)」陷阱

閉包能保留變數是優點,但如果用錯,就會變成最可怕的 Bug:過時閉包 (Stale Closure)。

看這段常見的錯誤寫法:

function Counter() {
  const [count, setCount] = useState(0);

  const handleAlert = () => {
    setTimeout(() => {
      // 3 秒後印出 count
      alert(`Count is: ${count}`);
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>加 1</button>
      <button onClick={handleAlert}>3秒後 Alert</button>
    </div>
  );
}

實驗步驟:
1. 點擊「3秒後 Alert」。
2. 在 3 秒內快速點擊「加 1」三次(此時畫面上的 Count 變成 3)。
3. 結果:3 秒到的時候,Alert 彈出的數字是 0!

為什麼會這樣?
React 的每一次渲染,都有它獨立的 State 快照與作用域。

當你按下「3秒後 Alert」時,handleAlert 閉包捕捉的是當下那一幀 (count = 0) 的作用域。

隨後你點擊加 1,React 觸發了全新的渲染(產生了新的 count 變數),但原本那個 timer 裡面的閉包依然抓著舊那一幀的 0 不放。

四、 如何破除過時閉包?

  1. Functional Update (函數式更新)
    如果需要基於「最新的 State」進行修改,請傳入 updater function:
//  捕捉舊閉包的 count
setCount(count + 1);

//  直接從參數拿到最新的 state
setCount(prevCount => prevCount + 1);
  1. 使用 useRef
    useRef 回傳的是一個固定記憶體指標的物件 ({ current: value }),它不隨渲染閉包改變:
const countRef = useRef(count);
countRef.current = count; // 每次渲染更新 current

setTimeout(() => {
  alert(`Count is: ${countRef.current}`); // 永遠讀到最新值
}, 3000);

上一篇
DAY 2 傳值還是傳址?從記憶體模型看 React 為什麼強調「不可變性 (Immutability)」
下一篇
DAY 4 解構賦值與展開運算子:寫出乾淨 State 操作語法的必修課
系列文
React 觀念架構:從js 基礎到Hook 底層邏輯 7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言