JavaScript 闭包

作者: 韩宝亿 | 来源:发表于2016-12-13 15:43 被阅读620次

    对于JavaScript的学习来说,闭包这一块一直是玄而又玄的东西,初学者很难掌握,我是学了大半年的JS,现在又回过来看【闭包】这部分,这里稍微总结一下,我采用由浅入深一步步的方式讲解,为了能帮助初学者们更好的理解,我将从下面几个方面展开:

    • JavaScript在函数中定义其他函数的能力;
    • 传递函数对象的方式;
    • 在函数内部和外部定义的变量的作用域;
    • 由变量作用域和闭包导致的常见问题;
    • 在jQuery中使用函数;
    • 函数交互导致的内存问题。

    1.创建内部函数

    支持内部函数的语言允许我们在必要的地方集合小型实用函数,以避免【污染命名空间】。
    所谓内部函数,就是定义在另一个函数中的函数


    Paste_Image.png

    innerFn()就是一个被包含在outerFn()作用域中的内部函数。这意味着在outerFn()内部调用innerFn()是有效的,而在outerFn()外部调用innerFn()则是无效的。


    Paste_Image.png
    这种技术特别适合于小型、单用途的函数。例如,【递归】通常最适合通过内部函数来表达

    1.1在任何地方调用内部函数

    JavaScript中的内部函数能够逃脱定义它们的外部函数。

    • 方法一,将内部函数指定给一个【全局变量】:


      Paste_Image.png

      在函数定义之后调用outerFn()会修改全局变量globalVar,此时它引用的是innerFn()。这意味着,后面调用globalVar()的操作就如同调用innerFn()一样,也会执行输出消息的语句。

    • 方法二,也可以通过在父函数中【返回值】来“营救出”内部函数的引用:


      Paste_Image.png

      这里从outerFn()中返回了一个对innerFn()的引用。通过调用outerFn()能够取得这个引用,而且,这个引用可以保存在变量中,也可以自己调用自己,从而触发消息输出。

    JavaScript的机制是只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数,只有最后一个变量废弃,它的【垃圾收集器】才能出面释放相应的内存空间。

    1.2理解变量作用域

    内部函数也可以拥有自己的变量,只不过这些变量都被限制在内部函数的作用域中:


    Paste_Image.png

    每当通过引用或其他方式调用这个内部函数时,都会创建一个新的innerVar变量,然后递增。

    内部函数可以像其他函数一样引用全局变量:


    Paste_Image.png

    现在,每次调用内部函数都会持续地递增这个全局变量的值。

    当这个变量是父函数的局部变量时,因为内部函数会继承父函数的作用域,所以内部函数也可以引用这个变量:


    Paste_Image.png

    通过每个引用调用innerFn()都会独立地递增outerVar。因为第二次函数调用的作用域中创建并绑定了一个新的outerFn()的【实例】。

    当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个【闭包】。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放,因为闭包仍然需要使用它们。

    2.处理闭包之间的交互

    当存在多个内部函数时,很可能会出现意料之外的闭包。假设我们又定义了一个递增函数,这个函数中的增量为2:


    Paste_Image.png

    这里,我们通过【对象】返回两个内部函数的引用调用任何一个内部函数。

    这两个内部函数引用了同一个局部变量,因此它们共享同一个【封闭环境】。当innerFn1()为outerVar递增1时,就为调用innerFn2()设置了outerVar的新的起点值,反之同理。

    3.在jQuery中创建闭包

    3.1 $(document).ready()的参数

    由于我们通常把$(document).ready()放在代码结构的顶层,因而这个函数不会成为闭包。但是,我们的代码通常都是在这个函数内部编写的,所以这些代码都处于一个内部函数中:


    Paste_Image.png

    这里和前面的例子其实是一样的,只不过外部函数是传入到$(document).ready()中的一个回调函数。innerFn()引用了位于回调函数作用域中的readyVar,因此innerFn()及其环境就创建了一个闭包。两次调用这个内部函数,两次输出保持了readyVar的值。

    3.2绑定事件处理程序

    .ready()结构通常用于包装其他的jQuery代码,包括【事件处理程序】的赋值。因为处理程序是函数,它们也就变成了内部函数;而且因为这些内部函数会被保存并在以后调用,于是它们也会创建闭包:


    Paste_Image.png

    这里的变量counter可以被.click()处理程序中的代码引用,由于创建了闭包,每次单击按钮都会引用counter的同一个实例。也就是说,消息会持续显示一组递增的值,而不会每次都显示1。

    事件处理程序同其他函数一样,也能够共享它们的封闭环境:


    Paste_Image.png

    因为这两个函数引用的是同一个变量counter,所以两个链接的递增和递减操作会影响同一个值,而不是各自独立的值。

    3.3在循环中绑定处理程序

    • 从一个经典的错误说起:
      构造六个div,当点击一个div时,按照预期,应该打出相应div的值,但是它总是会显示div的数量。因为事件处理器函数绑定了变量i本身,而不是函数在构造时的变量i的值,i始终无法被释放。换句话说,即使在绑定处理程序i的值每次都不一样,每个click处理程序最终引用的i都相同,都等于单击事件实际发生时i的最终值(6)。
      Paste_Image.png
      解决这个问题的方式有很多。
    • 方法一:避免在循环中创建函数,它可能只会带来无谓的计算,还会引起混淆,正如上面那个经典的错误。我们可以先在循环外创建一个辅助函数,让这个辅助函数再返回一个绑定了当前i值的函数,这样就不会导致混淆了。改良后的例子,用正确的方式给一个数组中的节点设置事件处理程序


      Paste_Image.png
    • 方法二:我们用一个立即执行函数给它包住,我们不再依赖i,而是用另外一个变量n把它保留下来。


      Paste_Image.png

    4、 应对内存泄漏的风险

    4.1避免意外的引用循环

    闭包可能会导致在不经意间创建引用循环。因为函数是必须在内存中的对象,所以位于函数封闭环境中的所有变量也需要保存在内存中:


    Paste_Image.png

    这里创建了一个名为outerVar的对象,该对象在内部函数innerFn()中被引用。然后,为outerVar创建了一个指向innerFn()的属性,之后返回了innerFn()。这样就在innerFn()上创建了一个引用outerVar的闭包,而outerVar又引用了innerFn()。


    Paste_Image.png
    这里我们修改了innerFn(),使它不在引用outerVar。但是,这样做仍然没有断开循环。即使innerFn()不再引用outerVar,outerVar也仍然位于innerFn()的【封闭环境】中。由于闭包的原因,位于outerFn()中的所以变量都隐含地被innerFn()所引用。因此,闭包会使意外的创建这些引用循环变得易如反掌。

    4.2 控制DOM与JavaScript的循环

    旧版本IE中存在一种难以处理的引用循环问题。当一个循环中同时包含DOM元素和常规的JavaScript对象时,IE无法释放任何一个对象,除非关闭浏览器。

    Paste_Image.png

    当指定单击事件处理程序时,就创建了一个在其封闭的环境中包含button变量的闭包。而且,现在的button也包含一个指向闭包(onclick属性自身)的引用。这样,就导致了在IE中即使离开当前页面也不会释放这个循环。
    【解决方法】:

    Paste_Image.png

    因为hello()函数不再包含button,引用就成了单向的(从button到hello)、不存在的循环,所以就不会造成内存泄漏了。

    【用jQuery化解引用循环】

    Paste_Image.png

    即便此时仍然会创建一个闭包,并且也会导致循环,但这里的代码却不会使IE发生内存泄漏。因为jQuery会手动释放自己指定的所有事件处理程序。只要坚持用jQuery的事件绑定方法,就无需为这种特定的常见原因导致的内存泄漏而担心。

    相关文章

      网友评论

      • 方应杭:4.1 如果这样会内存泄露,你就应该去跟浏览器开发者报 bug
      • 长青之木:引用循环:

        function outerFn(){
        var outerVar = {};
        function innerFn(){
        console.info(outerVar);
        }

        outerVar.fn = innerFn;
        return innerFn;
        }
        并不会造成内存泄露吧 ?
        长青之木:@饥人谷_韩宝亿 l你没有变量再引用它,就会被回收。
        它内部循环引用也一样会被回收。
        我觉得是这样的
        韩宝亿:@lifeso 但是他return出来了后它并不会销毁吧
      • 学着放下:很不错的文章

      本文标题:JavaScript 闭包

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