参考:
https://en.wikipedia.org/wiki/Closure_(computer_programming)#Implementation_and_theory
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
https://stackoverflow.com/questions/111102/how-do-javascript-closures-work
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 this.code(
{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)
...
网友评论