Joseph Chen
一、什麼是作用域(Scope)?
作用域(Scope)決定了「一個變數在哪裡可以被讀取或修改」。 JavaScript 有三種作用域層級,理解它們是搞懂閉包的第一步。
全域作用域
Global Scope
在任何函式或區塊之外宣告的變數,程式碼的任何地方都能存取。
函式作用域
Function Scope
在函式內部宣告的變數,只有函式內部可以存取,外部無法讀取。
區塊作用域
Block Scope
用 {} 包起來的區塊內,let / const 宣告的變數只在該區塊內有效。
二、var / let / const 與作用域的本質差異
這三個關鍵字的差異不只是「能不能重新賦值」,最根本的差別在於作用域的大小和提升(Hoisting)的行為。
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函式作用域 | 區塊作用域 | 區塊作用域 |
| 可重新賦值 | ✅ 可以 | ✅ 可以 | ❌ 不行 |
| 可重新宣告 | ✅ 可以 | ❌ 不行 | ❌ 不行 |
| Hoisting | ✅(值為 undefined) | ✅(TDZ,存取報錯) | ✅(TDZ,存取報錯) |
| 全域物件屬性 | ✅ window.xxx | ❌ 不會掛上 | ❌ 不會掛上 |
Hoisting(提升)與暫時死區(TDZ)
Hoisting 是指 JS 引擎在執行程式碼之前,會先把所有宣告「提升」到作用域頂部。 但 let 和 const 雖然也會提升, 但在宣告那行之前讀取它們會進入暫時死區(Temporal Dead Zone),直接報錯。
三、作用域鏈(Scope Chain)
當 JS 查找一個變數時,它會先在目前作用域找,找不到就往外層作用域找, 一層一層向上,直到全域作用域為止。這個查找鏈就叫做作用域鏈。
四、閉包(Closure)深度解析
閉包是 JS 中最常被問到也最常被誤解的概念。簡單說,閉包就是:「一個函式,記得它被定義時所在的作用域,即使那個作用域已經執行完畢。」
閉包形成的三個條件
有一個外層函式(提供作用域)
外層函式內部定義了一個內層函式
內層函式引用了外層函式的變數,且被回傳或傳遞到外部使用
閉包的實際應用場景
私有變數(Private State)
JS 沒有原生的 private 關鍵字(class 的 # 是後來才加的), 閉包是在 ES6 之前實現私有狀態的標準做法。
函式工廠(Function Factory)
用閉包「預先固定」部分參數,產生一系列相關函式。這在 React 的事件處理中非常常見。
快取(Memoization)
五、最常見的閉包陷阱
var 在迴圈中建立閉包
這是最經典的面試題。用 var 的迴圈,所有閉包共享同一個變數。
這個問題的根本原因是 var 的函式作用域。用 let 替換 var,讓每次迭代都有獨立的區塊作用域,閉包就能各自記住正確的 i 值。
閉包造成的記憶體洩漏
閉包會讓外層函式的變數一直存在記憶體中,如果閉包持有大型資料且沒有釋放,會造成記憶體洩漏。
閉包不能「記住」this
閉包記住的是外層作用域的變數,但 this 不是變數,它是由呼叫方式決定的。 這是一個不少人混淆的地方。
this,它會繼承定義時外層作用域的 this。 這就是 React 事件處理器通常用箭頭函式的原因——可以正確讀取元件實例的 this。六、核心重點整理
作用域決定變數在哪裡可以被讀取,分全域/函式/區塊三層
作用域鏈找不到變數就往外層找,一直找到全域;基於詞法作用域(寫程式的位置決定)
var函式作用域、會 Hoisting(值為 undefined)、可重複宣告 → 避免使用
let / const區塊作用域、TDZ(宣告前存取報錯)、let 可重新賦值,const 不行
閉包內層函式記住外層作用域變數引用,即使外層函式已執行完畢
var + 閉包迴圈所有閉包共享同一個 var 變數 → 用 let 解決,各次迭代有獨立作用域
this ≠ 閉包this 由呼叫方式決定,不被閉包記住 → 箭頭函式繼承外層 this