程序员

function closure: 理解函数闭包和它的实现原理

2018-12-15  本文已影响2人  davidhuangdw

参考:
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

closure引发的坑

  1. 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

  1. 局部变量只要还被子函数引用,在子函数释放前就不会被释放:

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个问题:

  1. 为什么外层函数执行完,局部变量(弹出stack)还能被访问?

    • 因为: 局部变量根本不在stack上而是在heap上, stack只放了指向局部变量表的指针
      • 必需支持GC: 需要靠GC来释放这段被分配在heap上的局部变量表
  2. 为什么函数在其他地方调用时却能访问到这些外层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

模拟编译器:

  1. 把closure引用改成显示的引用
  2. 把局部变量表分配在heap上而不是stack上
  3. 声明函数的地方创建函数对象,并且把父级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)
...

上一篇下一篇

猜你喜欢

热点阅读