Closure 是 ECMAScript 的一大特色,但由於對其不了解,因此很多人不敢使用 Closure;或者雖然會使用 Closure,但仍然對其原理一知半解。本文以 Runtime 角度深入探討 Closure 底層機制,讓我們徹底了解 Closure 的黑魔法。
Version
macOS Catalina 10.15
VS Code 1.38.1
Quokka 1.0.254
ECMAScript 5
ECMAScript 2015
Definition
Closure
Function 會將其 lexical scope 與 local scope 的變數封裝成新的 function scope,儘管該 function 已經離開原本定義的 scope,依然能夠抓到的 lexical scope 的變數成功執行
ECMAScript 5
function makeCount(init) {
return function(step) {
return init += step;
}
};
var count = makeCount(2);
count(1); // ?
count(2); // ?
count(3); // ?
ES5 以 function declaration 定義 makeCount()
,回傳 anonymous function 給 count
。
ECMAScript 2015
let makeCount = init => step => init += step;
let count = makeCount(2);
count(1); // ?
count(2); // ?
count(3); // ?
這是最簡單的 closure,雖然 ES5 與 ES6 的寫法有一點點差異。
init
為 makeCount()
的 argument,定義在 makeCount()
的 local scope 內,最後 return anonymous function 給 count
。
當 count()
執行時,還能抓到 init
,這就是 Closure。
Myth of Stack
我們知道 function 的 parameter 屬 local scope,在執行時期都是存放在 stack。
執行 makeCount()
時,init
parameter 是存放在 stack,當 makeCount()
回傳 anonymous function 給 count
後,stack 會釋放,因此 init
parameter 也會被釋放,也就是 makeCount()
的 local scope 已經不存在,為什麼再次執行 count()
時,還會抓到 init
parameter 呢 ?
Scope Chain
在 ECMAScript 中,所有的 variable 都是定義在 scope
object 內,由 key / value 構成,也因為是 object,scope
是在 heap 內。
function makeCount(init) {
return function(step) {
return init += step;
}
};
在 code 執行前,ECMAScript 會將所有 variable 存在 global scope object 內。
因此我們有 key 為 makeCount
,而 value 指向 function 的定義 object。
比較特別的是:每個 function object 有 hidden property [[scope]]
指向定義該 function 的 scope object。
makeCount(2);
當執行 makeCount(2)
時,會有自己新的 function scope,因此會建立新的 scope object。
新的 scope object 一樣是由 key / value 構成,除了有 function 該有的 this
、arguments
外,還有 init
parameter。
雖然我們是直接 return anonymous function,仍然可以想成有一個 key 為 f
,而 value 指向 function 的定義 object,他也有[[scope]]
指回 makeCount()
的 scope object。
比較特別的是:新的 function scope object 有一個 property : outer
指向定義makeCount()
的 global scope object,這個 reference 正是由 [[scope]]
複製而來。
let count = makeCount(2);
在 global scope object 的 key 新增 count
,其 value 指向所 return 的 anonymous function。
count(1);
當執行 count(1)
時,又有新的 scope object 建立,一樣有 this
與 arguments
,也有 step
parameter 為 1
。
且 outer
property 亦由 [[scope]]
複製而來,因此可以指向 makeCount()
的 scope object。
當要執行 init += step
時,在 count()
的 scope object 並沒有 key 為init
的 property,怎麼辦呢 ?
此時 ECMAScript 會透過 outer
property 往上找,在 makeCount()
scope object 找到 init
。
由於 scope object 會透過 outer
與 [[scope]]
一層一層往上找,因此稱為 Scope Chain,這是 closure 之所以能正常執行的原因。
Conclusion
- Scope object 因為是放在 heap,而不是放在 stack,因此不會隨著 function 執行完而釋放
- 只要 scope object
有
被其他 scope object 的outer
property 所參考,則 ECMAScript runtime 的 garbage collection 就不會釋放該 scope object,因此 closure 可以正常執行 - closure 能讀取 function 外部的變數,也稱為 lexical scope
Reference
Adam Breindel, JavaScript Scope Chains and Closures
Dr.Axel Ranschmayer, Speaking JavaScript