給自己看的 JavaScript 進階 - closure
— 程式導師計劃, coding 筆記, JavaScript — 6 min read
給自己看的 JS 進階:(建議按照順序看) 給自己看的 JS 進階-變數 給自己看的 JS 進階-Hoisting 給自己看的 JS 進階-Closure 給自己看的 JS 進階-物件導向
Closure 閉包
先看一個例子:
function test() { var a = 10 function inner() { a++ console.log(a) } return inner // 回傳 inner 這個函數}
var func = test()fanc() // 也就是 inner(), 11fanc() 12fanc() 13
所謂 Closure 閉包
就是像這樣,在一個 function 中 return 一個 function 。當我們呼叫裡面的 function 時,裡面的 function 會將外面 finction 的值記起來並鎖在裡面,因此稱為閉包。
如果我們希望能記住上次計算的值,不用再算一次,就可以使用閉包,例如:
function complex(n) { // 複雜計算 console.log('caculate'); return n * n;}
function cache(func) { var ans = {}; return function (num) { if (ans[num]) { // 如果有紀錄就直接回傳值 return ans[num]; }
ans[num] = func(num); return ans[num]; };}
const cacheComplex = cache(complex);console.log(cacheComplex(20)); // caculate 400console.log(cacheComplex(20)); // 400console.log(cacheComplex(20)); // 400
只要算過一次 ans[num]
就會被記起來,之後就都不用再跑一次 complex 了。
Closure ㄉ原理
ECMAScript ES3 版本中有提到,每個 CE 都有一個 Scope Chain
,進入 EC 時, Scope Chain
被初始化為 Activation Object
(其實就是 function 中的 VO
) 和 [[scope]]
。
看一個簡單的例子:
var a = 1;function test() { var b = 2; function inner() { var c = 3; console.log(a); // 1 console.log(b); // 2 } inner();}
test();
此時底層的狀態是:
Global EC: { VO: { a: undefined, test:func }, scopeChain: [Global VO]}
// 初始化一下test.[[Scope]] = globalEC.scopeChain // [Global.VO]
進到 testEC 之後如下:
test EC: { AO: { b: undefined, inner: func }, scopeChain: [testEC.AO, test.[[Scope]]] // 看上一格,也就是 [testEC.AO, globalEC.scopeChain] // 也就是 [testEC.AO, Global.VO]}
// 初始化一下INNER.[[Scope]] = testEC.scopeChain // [testEC.AO, Global.VO]
Global EC: { VO: { a: 1, test: func }, scopeChain: [Global.VO]}
最後進入 innerEC:
inner EC: { AO: { c: undefined, }, scopeChain: [innerEC.AO, inner.[[Scope]]] // 也就是 [innerEC.AO, testEC.AO, Global.VO]}
test EC: { AO: { b: 2, inner: func }, scopeChain: [testEC.AO, test.[[Scope]]] // 也就是 [testEC.AO, Global.VO]}
Global EC: { VO: { a: 1, test: func }, scopeChain: [Global.VO]}
此時回到一開始的例子:
function test() { var a = 10; function inner() { a++; console.log(a); } return inner; // 回傳 inner 這個函數}
var func = test();fanc(); // 也就是 inner()
最後一行執行時 test EC
已經結束,本來底層機制應該要全部拿掉,但因為 innerEC.scopeChain
是 [innerEC.AO, testEC.AO, Global.VO]
,因此 testEC.AO
還不能那麼快退場。這就是為 什麼 inner 可以拿到上一層變數值並儲存更改的原因。
不過偶爾閉包也會產生一些問題,例如外層包了超大的物件,就算之後只使用內層,因為關聯外層的 AO ,那個超大物件就無法被回收。
Closure 的小陷阱
var arr = [];for (var i = 0; i < 5; i++) { arr[i] = function () { console.log(i); };}
arr[0]();
本來預期會得到一到五,結果出來卻只有 5 。
此處的 i 是一個 global 的變數,當我們呼叫 arr[0]()
時,是 進到 for 迴圈中拿函數,因此函數中的 scope chain 會連動到 global 的 AO ,呼叫時 for 迴圈已經跑完,所以 global.VO
中 i 的值是 5 ,延用該 VO 的函數自然而然會輸出 5 。
解決方法:
function logN(n) { // 閉包的概念 return function () { console.log(n); };}
const log2 = logN(2);log2(); // 2
var arr = [];for (var i = 0; i < 5; i++) { arr[i] = logN(i);}
arr[0](); // 0
因為 arr[i]
會迴傳一個新的 function ,因此會產生新的作用域去記住傳入的值。
也可以使用 IIFE
,也就是立即呼叫函式,例如:
(() => { console.log(123);})(); // 立刻執行,輸出 123
回到剛剛的問題,我們也可以把剛剛的函式 logN
放進去:
var arr = [];for (var i = 0; i < 5; i++) { arr[i] = (function (num) { return function () { console.log(num); }; })(i);}
arr[0](); // 0
也可以直接這樣寫:
var arr = [];for (let i = 0; i < 5; i++) { arr[i] = function () { console.log(i); };}
arr[0]();
因為 let
的作用域只存在 block 中,迴圈等於是跑了五個 block arr[i] = function() { console.log(i) }
,每一圈都有自己的作用域,所以呼叫 arr[0]
時自然就找到印出 0 的函數。
Closure 的範例
Closure 隱藏資訊不被額外操控的時候很好用,例如:
var money = 99;
function add(num) { money += num;}
function deduct(mun) { money -= num;}
add(1);deduct(10);console.log(money); // 90
// 這時你同事很壞,加了一行money -= 90;// 就算繞過任何操作還是可以改變 money 的值
此時就可以使用 Closure:
function createWallet(init) { var money = init return { add: function(num) { money += num }, deduct: function(num) { money -= num } }, getMoney() { return money }}
var myWallet = createWallet(99)myWallet.add(1)myWallet.deduct(10)console.log(myWallet.getMoney()) // 90