今天我們要來介紹 Closure 的經典例子。
Closure 經典例子
我們先建立一個 buildFunctions function,並在當中建立 arr 的空陣列和 for 迴圈,在迴圈中我們使用 .push()
加入 function 到陣列。
而在 function 中我們輸出 i
,i
會透過範圍鏈參照到,這樣會執行三次,並新增三個函式表達式 (函式物件) 給陣列,這樣陣列就會有三個相同的 function,最終 buildFunctions 會回傳 arr。
陣列是任何東西的集合,我們可以加入 function 到陣列中。
1 | function buildFunctions() { |
現在我們呼叫 buildFunctions 來取得陣列,賦予到 fs 的變數中,而在陣列中有 function,我們能透過陣列來呼叫它們,來猜猜這三個 function 輸出的 i 分別會是什麼 ?
我們要記得,在
arr.push
時,不是在呼叫 function,只是在建立它,然後放到陣列中,function 是在別處呼叫的。
1 | var fs = buildFunctions(); |
當 function 呼叫後,會去尋找 i
,會透過範圍鏈參照到 i
,而這裡是讓大家都驚訝的部分,當我們看這個程式碼,會預期輸出 0
, 1
, 2
,但結果全都是 3
,為什麼參照到的 i
都是 3
?
產生的問題
這是我們的程式碼,我們有 buildFunctions function,會把函式表達式加入到陣列中,然後在下面呼叫 function,所以我們會有陣列包含三個函式表達式,每個函式表達式會輸出 i
,當我們執行都會輸出 3
。
1 | function buildFunctions() { |
發生什麼事
我們來看看執行堆發生什麼事。
我們知道全域執行環境會最先被建立,它包含 buildFunctions function 和 fs 變數。
1 | ----------------------------------------------------- |
當執行到 fs = buildFunctions()
時,會呼叫 buildFunctions,建立新的執行環境,裡面會有兩個變數,分別是 i
(在 for 迴圈建立) 和 arr
(在 function 中宣告)。
然後當 for 迴圈執行時,i
一開始會是 0,會將函式表達式放到陣列中,這部分就是大家常常搞混的地方,我們要知道這裡的 function 是不會執行的,只是建立函式物件而已,之後繼續執行,i
會變成 1
,因為 i++
的關係,然後新增新的函式表達式到陣列中,之後 i
變成 2
,又執行一次,最後 i++
執行,i
為 3
,JavaScript 引擎看到 i < 3
,發現 i
不小於 3
,離開 for 迴圈。
這裡 i
離開時值是 3
,當執行到 return arr
時,在記憶體中,i
為 3
,陣列裡會有三個 function (匿名的,用 f0
, f1
, f2
會比較好辨認)。
1 | ----------------------------------------------------- |
然後我們回到全域執行環境中,buildFunctions 就會離開執行堆,還記得我們學過的 Closure 嗎 ?
JavaScript 會幫我們保留可以取用到的變數,記憶體仍存在這兩個變數。
1 | ------------------------ |
然後到第一個呼叫 function 的地方,執行 fs[0]
,新的執行環境建立,由於沒有變數 i
,所以透過範圍鏈尋找,到 buildFunctions 的執行環境的記憶體內找到 i
,i
為 3
,所以就輸出 3
,然後離開執行堆。
1 | ----------------------------------------------------- |
接著執行 fs[1]
,執行環境建立,與 fs[0]
參照相同的外部環境,因為和 fs[0]
同樣都建立在 buildFunctions 內,所以當 fs[1]
尋找 i
,i
也是 3
,所以會輸出 3
,當然,最後的 fs[2]
也是一樣的。
1 | ----------------------------------------------------- |
這就像父母有三個小孩一樣,你問他們的父母幾歲,他們不會因出生的時候不同,而回答不一樣的年齡,他們會給你相同的答案,同樣的這三個 function,當我們執行它們時,會告訴我們外部環境參照到父環境 (parent context) 在記憶體中的值,所以這三個輸出的值都是一樣的,因為它們參照到相同的記憶體位置。
另外,當我們執行 function,仍可以取得它的外部變數,也稱為自由變數 (free variable)。
自由變數 (Free Variable) 是在 function 外,但仍能取用的變數。
輸出 0
, 1
, 2
- 透過 ES6 的
let
變數
let
的範圍是在 {}
中,每次 for
迴圈執行時,let
在記憶體中會是新的變數,所以當 function 被呼叫時,在執行環境中 let
會是不同的記憶體位置,每次都會指向不同的記憶體,它們本質上是不同的變數。
1 | function buildFunctions2() { |
- ES5 的 IIFE
為了保存 i
的值,我們需要將 push
到陣列的函式表達式的執行環境分開,需要父作用域 (Parent Scope) 保留迴圈執行時當前的 i
值,而唯一獲得執行環境的方法是執行 function,那我們該如何立即執行 function 呢 ?
沒錯,就是 IIFE
當我們在 push
時,立即呼叫 function,將 i
作為參數傳入到 IIFE 中,而 IIFE 則透過 j
來接收 i
的值,每次迴圈執行時,會立即執行 function,建立自己的執行環境,j
會分別存在這三個執行環境中,所以會分別有 j = 0
, j = 1
, j = 2
的執行環境,即使執行環境只執行一行便結束,我們仍有 Closure 會保留 j
的值。
透過 IIFE 回傳函式表達式,這個函式表達式會被 push
到陣列中,而當他們被呼叫時,不需要到迴圈中找值,只要到執行環境中找便可,j
的值會在迴圈執行時儲存,這是利用 Closure 的優勢,確保呼叫時能取得正確的值。
1 | function buildFunctions2() { |
總結
我們應用了之前所學的所有知識,像是 first-class function、Closure 這些概念,如果我們了解 Closure 是如何運作,就等於了解進階 JavaScript 程式設計的重要部分,而在其他方面 function Closure 也是相當實用的。