美文网首页程序员
function closure: 理解函数闭包和它的实现原理

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

作者: davidhuangdw | 来源:发表于2018-12-15 21:04 被阅读2次

    参考:
    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引发的坑

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

    相关文章

      网友评论

        本文标题:function closure: 理解函数闭包和它的实现原理

        本文链接:https://www.haomeiwen.com/subject/uctzhqtx.html