回顧 JavaScript 核心 X Bsic Review - Closure
本篇複習關於 JavaScript 最基礎的知識點:Closure。
閉包的定義
一個子函式可以記住且持續存取外部作用域的變數,即便脫離父層函式的執行環境仍可在外部取用或執行。
閉包的特色包含:
- 子函式(Inner Function)
- 外部變數(Outer Variables)
- 定義當下的作用域(Lexical Environment),每次呼叫會創造一個新的作用域
- 垃圾回收機制(GC)
很重要的一點在「保護私有變數不為外部修改或取用」!
1 | function outer() { |
應用的實際範例
曾需要在專案內根據「投資年限」的長短計算手續費率,得出投資長達 1~該投資年(investYear) 的費率變化:
1 | function generateFeeRate () { |
再根據客戶的投資年限 + 投資本金去計算出費用圖表:
1 | function reCalculateAssetData () { |
封裝費率的值,來得出不同投資年分的資產總值計算費用:
1 | const fee = this.generateFeeRate(); // 初始化 |
實戰框架的應用
在 Vue React 的應用可謂遍地開花:
案例類型 | 案例範例 | 說明 |
---|---|---|
事件監聽器 (Event Handlers) | onClick={() => handleClick(item.id)} |
這個 item.id 是靠 closure 記住的! |
Hooks (React) | useState , useEffect , 自訂 hooks |
useEffect 裡的回呼函式會「記住」當下的變數狀態。 |
Composition API (Vue3) | setup 裡 return 出來的函式 |
setup() 裡定義的變數,被 ref 或方法捕捉到。 |
工廠函式 (例如 debounce、throttle) | useDebounce(fn, delay) |
debounce 內部會記住 timer id。 |
表單驗證器生成 | 比如 createValidator() 回傳自訂驗證函式 |
Validator 會 closure 捕捉驗證條件。 |
回傳 function 的設計 | service, composable, helper | 一個函式 return 出另一個帶著設定的函式。 |
React
每個元件本身都是一個父層作用域,內部定義的 handler methods 都可以提取定義在父層作用域的變數:
1 | function Counter() { |
Vue3 Setup
<script setup>
標籤是一個封裝的 Setup(){}
函式,本身是一個父層作用域,所以內部定義的 handler methods 都可以提取定義在父層作用域的變數:
1 | <script setup> |
Vue2 This
Vue2 的變數資料是透過 Object.defineProperty
實作 Proxy-like 對每個屬性建立 getter/setter,註冊在 data 物件後透過代理在 this
上面的行為來取得資料。
1 | // 定義資料物件 |
所以 Vue2 資料註冊的方式不是透過 Closure,而是透過訂閱更新的 this
獲取最新資料!
工廠函式與閉包
先定義了一個具有閉包的函式後,透過「呼叫函式保留了該作用域並封存變數」,以產生一個或多個保有各自私有狀態的工廠函式;換句話說,工廠函式需要透過閉包的手法建立。
1 | function createCounter() { |
三方套件與 IIFE 閉包
IIFE(Immediately Invoked Function Expression)之所以常被用來定義私有變數,正是因為閉包的特性。
是在 ES6 模組 (import/export) 出現前,常見的私有變數封裝技巧。
例如封裝模組:
1 | const Counter = (function () { |
來幾個老朋友閉包陷阱題
For loop 系列
經典 for 迴圈 + setTimeout 問題
for loop 可以看做一個 外部執行環境,內部的 setTimeout callback
自然形成一個閉包子函式:
1 | for (var i = 0; i < 3; i++) { |
答案預期輸出什麼?
1 | 3 |
為什麼?
var
是 function-scope,每一圈跑完都會汙染i
的值,最後i = 3
(包含迴圈最後執行 i++)setTimeout
是宏任務會最後執行。
解法 1 - let
1 | for (let i = 0; i < 3; i++) { |
答案預期輸出什麼?
1 | 0 |
var
改成let
會套用 block-scope 屬性,就可以讓每一次的迴圈重新建立自己的lexical scope
。
解法 2 - IIFE
1 | for (var i = 0; i < 3; i++) { |
立即呼叫函式的閉包特性始得私有變數 i
在每一次呼叫時立刻封裝起來,讓子函式使用獨立的 j
,所以可以完美的更新正確的變數。
閉包的廣義
所有函式本質上都可能是閉包,因為它們都帶有「定義當下的作用域鏈」(Lexical Scope Chain)。
只是真正重要的閉包行為,是當函式「離開原本的作用域」,仍然「記得當初的變數」。