function closure: 理解函式閉包和它的實現原理
closure是什麼?
function closure是一個語言特性, 1960s出現在schema等函式式語言上,現代語言(ruby/python/js/java ...)大多支援。
closure(特性)指的是 -- 函式可以讀寫(宣告它的)外層函式的區域性變數, 即使外層函式已經執行完畢。
以js為例看幾個例子:
let print = console.log; // Example 1: callback functions: let count = 0; let id = setInterval(()=>{ print(++count) }, 1000);// callback access outer var: count setTimeout(()=> clearInterval(id), 5000);// callback access outer var: id // Example 2: high order function: let mul = a => b => a*b; // closure: doub, trip functions(b=>a*b) can access outer local variable: a let doub = mul(2); let trip = mul(3); print(doub(10));// 20 print(trip(10));// 30 // Example 3: function builder: function makeCounter(count=0, step=1){ let calls = 0; return { inc: () => { calls++; return count+=step; }, dec: () => { calls++; return count-=step; }, getCalls: ()=> calls, getCount: ()=> count, } } // closure: inc, dec, getCalls as functions can access outer local variables: count, step, calls let {inc, dec, getCalls} = makeCounter(); print(inc());// 1 print(inc());// 2 print(dec());// 1 print(getCalls());// 3
-
注意:
- '外層函式'指的是宣告它的函式,也就是肉眼看到的外層函式, 而不是呼叫它的函式
-
我們把每一層函式(區域性變量表)稱為一個lexical scope
- 不只是父級外層,所有祖先的外層的lexical scope都能訪問
- block也算一層
closure引發的坑
- closure中,函式引用到的是外部區域性變數本身,而不是外部區域性變數的值
// x has become 3 for all 3 callbacks: for(var x=0; x<3; x++) setTimeout(() => console.log(x));
這個例子中3個callbacks被呼叫時,x已經變成3了,所以輸出的都是3
- 區域性變數只要還被子函式引用,在子函式釋放前就不會被釋放:
function x(a){ function foo(){... a ...} // closure: access var a doSomething(foo); //'big' also be hold by foo, because 'big' is also x's local variable let big = fetchBigObject(); run1(big); run2(big); } // improved: function x(a){ function foo(){... a ...} // closure: access var a doSomething(foo); { // inside nested block, 'big' no longer belongs to x's local variables let big = fetchBigObject(); run1(big); run2(big); } }
編譯器如何實現closure的?
先思考2個問題:
-
為什麼外層函式執行完,區域性變數(彈出stack)還能被訪問?
-
因為: 區域性變數根本不在stack上而是在heap上, stack只放了指向區域性變量表的指標
- 必需支援GC: 需要靠GC來釋放這段被分配在heap上的區域性變量表
-
因為: 區域性變數根本不在stack上而是在heap上, stack只放了指向區域性變量表的指標
-
為什麼函式在其他地方呼叫時卻能訪問到這些外層lexical scope的區域性變數?
- 因為: 每次定義(宣告)函式實際上建立了一個新的函式物件, 不僅儲存程式碼位置的引用(相同程式碼段),還儲存指向父函式此刻的區域性變量表的引用(各不相同:因為父函式每次執行都建立一個新的區域性變量表)
根據以上以上2個結論,我們已經可以模擬編譯器來實現closure。
以下面的js程式碼(採用了closure)為例,我們模擬編譯器加塞額外邏輯來去掉closure引用,使得改造後的程式碼不僅沒用到closure而且執行時依然保持原來的邏輯。
原始程式碼:
function foo(){ let a = 1; function bar(){ let b = 2; a++; function baz(){ return a+b; } b++; return baz; } a++; return bar; } let bazFunc = foo()(); console.log(bazFunc());//6
模擬編譯器:
- 把closure引用改成顯示的引用
- 把區域性變量表分配在heap上而不是stack上
- 宣告函式的地方建立函式物件,並且把父級scope存進函式物件
// step 1: change implicit references to explicit ones function foo(){ let a = 1; function bar(){ let b=2; parent_scope.a++; function baz(){ return parent_scope.parent_scope.a + parent_scope.b; } b++; return baz; } a++; return bar; }
// step 2: allocate var_table on heap function foo(){ let var_table = {}; var_table.a = 1; function bar(){ let var_table={}; var_table.b=2; parent_scope.a++; function baz(){ return parent_scope.parent_scope.a + parent_scope.b; } var_table.b++; return baz; } var_table.a ++; return bar; }
// step 3(complete): assign parent_scope when create function object // (you can ignore 'this' in the following example) let global = this; function build(parent_scope, func){ return { parent_scope: parent_scope, code: func, run: function(that, ...args){ return func( {parent_scope: this.parent_scope, this: that}, ...args ) } } } const foo = build(global, function(scope, ...args){ scope.a = 1; const bar = build(scope, function(scope, ...args){ scope.b=2; scope.parent_scope.a++; const baz = build(scope, function(scope, ...args){ return scope.parent_scope.parent_scope.a + scope.parent_scope.b; }); scope.b++; return baz; }); scope.a ++; return bar; }); let bazFunc = foo.run(this).run(this); console.log(bazFunc.run(this));// 6
至此,step 3中已經沒有任何closure引用,但依然保持原始碼相同邏輯(以上例子中可忽略程式碼中的this,因為這個例子中並沒有被用到)。
思考題
下面是一段redux的原始碼:你能理解為什麼其中 {dispatch: (...args)=>dispatch(...args)} 不寫成 {dispatch: dispatch} 嗎?
// source code: https://github.com/reduxjs/redux/blob/master/src/applyMiddleware.js ... let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } const chain = middlewares.map(middleware => middleware({ ... dispatch: (...args)=>dispatch(...args)//!!why not "dispatch: dispatch" ? }) ) dispatch = compose(...chain)(store.dispatch) ...