关于闭包

作者: HungerLyndon | 来源:发表于2017-01-28 23:32 被阅读521次

    本文章著作权归饥人谷_Lyndon和饥人谷所有,转载请注明出处。

    闭包对于我而言是一个难点,但闭包又是一个很有用的知识点,很多高级应用都需要依赖闭包。
    所以在参考一些文章加上大量练习后,我来写一写自己理解闭包的过程,首先是弄清楚以下几个知识点。


    >>> Part 1. 变量的作用域

    JS中,变量的作用域只有两种:全局作用域、函数作用域。对应的变量也只有两种:全局变量、局部变量。

    函数内部可以直接读取全局变量。

    var a = 1;
    function f(){
        console.log(a);
    }
    f();  // 1
    

    但是函数外部无法读取到函数内部的局部变量。

    function f(){
        var a = 1;
    }
    console.log(a);  // Uncaught ReferenceError: a is not defined
    

    这一个Part是比较好理解的。


    >>> Part 2. 如何从外部读取到局部变量?

    在禄永老师的公开课中,老师将从外部读取局部变量这一情况称作“伟大的逃脱”。总结而言,有两种方法来实现。

    • 返回值的方法:函数作为返回值
    function f1(){
        var a = 1;
        function f2(){
            console.log(a);
        }
        return f2;
    }
    var result = f1();
    result();  // 1
    

    函数f2包裹在函数f1内,根据作用域链的原理:子对象会一级一级向上寻找父对象的变量,f1所有的局部变量都可以被f2访问到,反之则不行。因此只要把f2作为返回值,就可以在f1外部读取到其中的内部变量。

    • 句柄的方法:定义全局变量
    var innerHandler = null;
    function outerFunc(){
        var outerVar = 1;
        function innerFunc(){
            console.log(outerVar);
            var innerVar = 2;
        }
        innerHandler = innerFunc;
    }
    outerFunc();
    innerHandler();  // 1
    

    这一方法首先定义了一个值为null的全局变量innerHandler,然后让innerHandler等于函数内部的函数,函数内部的函数则可以通过作用域链访问到父对象的变量outerVar,之后在外部调用innerHandler的时候,就可以访问到outerFunc函数中的内部变量outerVar


    >>> Part 3. 闭包

    网络上有千万种对闭包的解释,其实闭包就是上面例子中的两个函数:f2以及innerFunc。书面解释就是:能够读取其他函数内部变量的函数

    在JS中,因为父函数内部的子函数才能够读取局部变量,因此闭包的常见形式就是:定义在函数内部的函数。前者是后者的充分不必要条件。

    一言以蔽之,闭包就是连接函数内部外部的渠道。


    >>> Part 4. 对示例代码段的解答

    • 第一段代码
    function outerFn() {
        console.log("Outer function");
        function innerFn() {
            var innerVar = 0;
            innerVar++;
            console.log("Inner function\t");
            console.log("innerVar = "+innerVar+"");
        }
        return innerFn;
    }
    
    var fnRef = outerFn();
    fnRef();
    fnRef();
    
    var fnRef2 = outerFn();
    fnRef2();
    fnRef2();
    

    在这一段代码当中,innerFn不是一个闭包,因为它并不需要读取其他函数的内部变量,唯一的变量innerVar就在innerFn函数内部。在第一个fnRef()之后,结果就是首先输出Outer function,然后输出Inner function,由于innerVar是函数innerFn的内部变量且自增,因此从0变为1,再输出innerVar = 1.

    这时候需要明白,当再次运行fnRef()时,由于fnRef本身已经变成了函数innerFn,所以其输出结果就不再有Outer Function这一句,而是直接输出:Inner function以及innerVar = 1.原因是此时的innerVar是一个内部变量,其作用域限定在innerFn函数中,每次调用执行innerFn函数,innerVar都会被重写。

    对于下面的fnRef2(),也是同理。最后的输出结果见下图:

    • 第二段代码
    var globalVar = 0;
    function outerFn() {
        console.log("Outer function");
        function innerFn() {
            globalVar++;
            console.log("Inner function\t");
            console.log("globalVar = " + globalVar + "");
        }
        return innerFn;
    }
    
    var fnRef = outerFn();
    fnRef();
    fnRef();
    
    var fnRef2 = outerFn();
    fnRef2();
    fnRef2();
    

    这里的globalVar是一个外部变量,也是一个全局变量,处于全局作用域下。所以当执行innerFn时,innerFn函数将会访问到一个每次都自增的全局作用域下的活动对象,因此输出的结果会从globalVar = 1一直到globalVar = 4.在执行间歇中,globalVar处于两个函数的作用域之外,天高地远谁也管不了,所以它的值会被保存在内存中,并不会立刻被抹去。最后的输出结果见下图:

    • 第三段代码
    function outerFn() {
        var outerVar = 0;
        console.log("Outer function");
        function innerFn() {
            outerVar++;
            console.log("Inner function\t");
            console.log("outerVar = " + outerVar + "");
        }
        return innerFn;
    }
    
    var fnRef = outerFn();
    fnRef();
    fnRef();
    
    var fnRef2 = outerFn();
    fnRef2();
    fnRef2();
    

    闭包来临了,这里的fnRef是一个闭包innerFn函数,但是此时的变量outerVar来到了父函数的作用域内,不像之前一样处于子函数作用域内或者处于全局作用域下。可以发现,这和Part 2中的例子非常相似。

    其原理是:外部函数的调用环境为相互独立的封闭闭包的环境,第二次的fnRef2调用outerFn没有沿用第一次调用fnRefouterVar的值,第二次函数调用的作用域创建并绑定了一个新的outerVar实例,两个闭包环境中的计数器是相互独立,不存在关联的。

    进一步来说,在每个封闭闭包环境中,外部函数的局部变量会保存在内存中,并不会在外部函数调用后被自动清除。原因在于:outerFninnerFn的父函数,而innerFn被赋值给一个全局变量,因此innerFn始终在内存当中,而它又依赖于outerFn,所以outerFn也必须始终在内存中,不会再函数被调用后就被抹去,因此闭包也有一点点不好,有可能造成内存泄漏。

    所以,结果应该是:outerVar = 1, outerVar = 2, outerVar = 1, outerVar = 2.结果如下图所示:

    我写到这自己已经完全明白了,我现在要用自己的理解来理顺一下最经典的问题。


    >>> Part 5. 理顺最经典问题

    <div id="divTest">
        <span>0</span>
        <span>1</span>
        <span>2</span>
        <span>3</span>
    </div>
    <script>
        var spans = document.querySelectorAll("#divTest span");
        for(var i = 0; i < spans.length; i++){
            spans[i].onclick = function(){
                console.log(i);
            }
        }
    </script>
    

    最经典的问题是:为什么我点击任何数字,控制台的输出结果永远是4?

    这里可使用作用域链来帮助理解,不妨将以上代码转化为:

    // function只是传递给了NodeList类型对象中的元素却并未执行,因为后面无括号
    spans[0] = function fn0(){console.log(i)};
    spans[1] = function fn1(){console.log(i)};
    spans[2] = function fn2(){console.log(i)};
    spans[3] = function fn3(){console.log(i)};
    
    globalContext = {
        AO: {
            i: undefined, // 0(fn0)1(fn1)2(fn2)3(fn3)4(终止循环)
            spans:[0], [1], [2], [3]
        },
        scope: null
    }
    fn0[[scope]] = globalContext.AO,
    fn1[[scope]] = globalContext.AO,
    fn2[[scope]] = globalContext.AO,
    fn3[[scope]] = globalContext.AO
    
    fn0Context = {
        AO:{
        },
        scope: fn0[[scope]]
    }
    
    fn1Context = {
        AO:{
        },
        scope: fn1[[scope]]
    }
    
    fn2Context = {
        AO:{
        },
        scope: fn2[[scope]]
    }
    
    fn3Context = {
        AO:{
        },
        scope: fn3[[scope]]
    }
    

    最后点击span元素的时候i早已变为4,因此永远输出4.

    改进的方法可以使用闭包,也就是:

    var spans = document.querySelectorAll("#divTest span");
    for(var i = 0; i < spans.length; i++) {
        spans[i].onclick = function(i){
            return function (){
                console.log(i);
            }
        }(i);
    }
    

    这个闭包也可以用作用域链来理解:

    globalContext = {
        AO:{
            i: undefined,
            spans: [0], [1], [2], [3]
        }
    }
    fn0.scope = globalContext.AO,
    fn1.scope = globalContext.AO,
    fn2.scope = globalContext.AO,
    fn3.scope = globalContext.AO
    
    fn0Context = {
        AO:{
            i: 0,
            function: anonymous
        }
        fn0[[scope]] = fn0.scope // globalContext.AO 
    }
    
    function_anonymousContext = {
        AO: {
        }
        function_anonymous[[scope]] = fn0Context.AO
    }
    ...
    

    >>> Part 6. 闭包的问题

    如同刚才的分析一样,当涉及到闭包时,函数中的变量都会被保存在内存中,因此需要避免滥用闭包,否则就有可能导致内存泄露。


    >>> 参考资料

    相关文章

      网友评论

        本文标题:关于闭包

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