zoukankan      html  css  js  c++  java
  • 浅谈闭包

     

    闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术。下来对其进行一个小小的总结

     

    什么是闭包?

     

    官方说法:

      闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量------《javascript高级程序设计第三版》

     

    下面就是一个简单的闭包:

    <span style="font-family: 'Microsoft YaHei';">function A(){
        var text="hello world";
        function B(){
           console.log(text);
        }
        return B;
    }
    var c=A();
    c(); // hello world </span>

    按照字面量的意思是:函数B有权访问函数A作用域中的变量(text),通过另一个函数C来访问这个函数的局部变量text。因此函数B形成了一个闭包。也可以说C是一个闭包,因为C执行的实际是函数B。

    这个需要注意的是,直接执行A();是没有任何反应的。因为return B没有执行,除非是return B();

    闭包的特性

     

     闭包有三个特性:

     1.函数嵌套函数
    2.函数内部可以引用外部的参数和变量
    3.参数和变量不会被垃圾回收机制回收

    解释一下第3点,为什么闭包的参数和变量不会被垃圾回收机制回收呢?

    首先我们先了解一下javascript的垃圾回收原理:

    (1)、在javascript中,如果一个对象不再被引用,那么这个对象就会被GC(garbage collection)回收; 

    (2)、如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。

      上面的示例代码中A是B的父函数,而B被赋给了一个全局变量C(全局变量的生命周期直至浏览器卸载页面才会结束),这导致B始终在内存中,而B的存在依赖于A,因此A也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

    闭包的作用:

     

      其实闭包的作用也是有闭包的特性决定的,根据上面的闭包特性,闭包的作用如下:

      1、可以读取函数内部的变量,而不是定义一起全局变量,避免污染环境

      2、让这些变量的值始终保持在内存中。

    闭包的代码示例

     

    下面主要介绍几种常见的闭包,并进行解析:

     

    demo1 局部变量的累加。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <span style="font-family: 'Microsoft YaHei';">function countFn(){
        var count=1;
        return function(){             //函数嵌套函数
            count++;
            console.log(count);
        }
    }
    var y = countFn();    //外部函数赋给变量y;
    y();   //2      //y函数调用一次,结果为2,相当于countFn()()
    y();   //3     //y函数调用第二次,结果为3,因为上一次调用的count还保存在内存中,没有被销毁,所以实现了累加
    y=null;  //垃圾回收,释放内存
    y();  // y is not a function
    </span>

    由于第一次执行完,变量count还保存在内存中,所以不会被回收,以致于第二次执行的时候可以对上次的值就行累加。当引入y=null时,销毁引用,释放内存

     

    demo2 循环中使用闭包


    代码如下(下面的三个代码示例):我们的目的是想在每次循环中调用循环序号:

     

    demo2-1

    复制代码
    for (var i = 0; i < 10; i++) {
        var a = function(){
            console.log(i)
        }
        a()  //依次为0--9
    }
    复制代码

    这个例子的结果是没有题的,我们依次打印出了0-9

    每一层匿名函数和变量i都组成了一个闭包,但是这样在循环中并没有问题,因为函数在循环体中立即被执行了

     

    demo2-2

    但是在setTimeout中就不一样了

    1
    2
    3
    4
    5
    6
    <span style="font-family: 'Microsoft YaHei';">for(var i = 0; i < 10; i++) {
        setTimeout(function() {
            console.log(i);  //10次10
        }, 1000);
    }
    </span>

    我们期望的依次是打印出0--10,实际情况是打印出 10次10。即使吧setTimeout的时间改为0,也是打印出10个10。这是为什么呢?

        这是因为setTimeout的一种机制,setTimeout是从任务队列结束的时候开始计时的,如果前面有进程没有结束,那么它就等到它结束再开始计时。在这里,任务队列就是它自己所在的循环。

    循环结束setTimeout才开始计时,所以无论如何,setTimeout里面的i都是最后一次循环的 i。该代码中,最后的 i 为10,所以打印出了10个10.

        这也就是为什么setTimeout的回调不是每次取循环时的值,而取最后一次的值

     

    demo2-3

    解决上面的setTimeout不能依次打印出循环的问题

    复制代码
    for(var i=0;i<10;i++){
        var a=function(e){
            return function(){
    
                console.log(e); //依次输入0--9
            }
        }
        setTimeout(a(i),0);
    }
    复制代码

    因为setTimeout第一个参数需要一个函数,所以返回一个函数给它,返回的同时把 i 作为参数传进去,通过形参 e 缓存了i,也就是说e变量相当于是 i 的一个拷贝 ,并带进返回的函数里面。

     setTimeout 的执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

     

    也可以用下面的写法,和上面类似:

    复制代码
    for(var i = 0; i < 10; i++) {
        (function(e) {
            setTimeout(function() {
                console.log(e);  //依次打印出0-9
            }, 0);
        })(i);
    }
    复制代码

     

     demo3 循环中添加事件

     看下面的一个典型的demo.

    我们希望每次点击li的时候,alert出li的索引值,所以用下面的代码:

    复制代码
     <ul id="test">
        <li>第一个</li>
        <li>第二个</li>
        <li>第三个</li>
        <li>第四个</li>
    </ul>
    
    var nodes = document.getElementsByTagName("li");
    for(i = 0,len=nodes.length;i<len;i++){
        nodes[i].onclick = function(){
            alert(i);   //值全是4
        };
    }
    复制代码

    事与愿违,无论点击哪一个li,都是alert(4),也就是都是alert循环结束之后的索引值。这是为什么呢?

        这是因为循环中为不同的元素绑定事件,事件回调函数里如果调用了跟循环相关的变量,则这个变量取循环的最后一个值。

        由于绑定的回调函数是一个匿名函数,所以上面的代码中, 这个匿名函数是一个闭包,携带的作用域为外层作用域(也就是for里面的作用域),当事件触发的时候,作用域中的变量已经随着循环走到最后了。

        还有一点就是,事件是需要触发的,而绝大多数情况下,触发的时候循环已经结束了,所以循环相关的变量就是最后一次的取值。

     

    要实现点击li,alert出li的索引值,需要将上面的代码进行以下的修改:

    复制代码
    <ul id="test">
        <li>第一个</li>
        <li>第二个</li>
        <li>第三个</li>
        <li>第四个</li>
    </ul>
    var nodes=document.getElementsByTagName("li");
    for(var i=0;i<nodes.length;i++){
        (function(e){
            nodes[i].onclick=function(){
                alert(e);
            };
        })(i)
    }
    复制代码

       解决思路: 增加若干个对应的闭包域空间(这里采用的是匿名函数),专门用来存储原先需要引用的内容(下标)。

       当立即执行函数执行的时候,e 值不会被销毁,因为它的里面有个匿名函数(也可以说是因为闭包的存在,所以变量不会被销毁)。执行后,e 值 与全局变量 i 的联系就切断了,

    也就是说,执行的时候,传进的 i 是多少,立即执行函数的 e 就是多少,但是 e 值不会消失,因为匿名函数的存在。 

     

    也可以用下面的解法,原理是一样的:

    复制代码
    <ul id="test">
        <li>第一个</li>
        <li>第二个</li>
        <li>第三个</li>
        <li>第四个</li>
    </ul>
    
    var nodes=document.getElementsByTagName('li');
    for(var i = 0; i<nodes.length;i++){
        (function(){
           var temp = i;
            nodes[i].onclick = function () {
                alert(temp);
            }
        })();
    }
    复制代码

     

     

    注意事项

     

      1、造成内存泄露

        由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以只有在绝对必要时再考虑使用闭包。

      2、在闭包中使用this也可能会导致一些问题。

           代码示例:来源于《js高级程序设计3》;

    其实我们的目的是想alert出object里面的name

    复制代码
     var name="The Window";
     var object={
         name:"My Object",
         getNameFunc:function(){
             return function(){
                 return this.name;
             }
         }
     }
     alert(object.getNameFunc()()); // The Window
    复制代码

     因为在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。

    每个函数在被调用时,都会自动取的两个特殊变量:this和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止。也就是说,里面的return function只会搜索

    到全局的this就停止继续搜索了。因为它永远不可能直接访问外部函数中的这两个变量。

     

    稍作修改,把外部作用域中的this对象保存在一个闭包能够访问的变量里。这样就可以让闭包访问该对象了。

    复制代码
     var name="The Window";
     var object={
         name:"My Object",
         getNameFunc:function(){
             var that=this;
             return function(){
                 return that.name;
             }
         }
     }
     alert(object.getNameFunc()()); // My Object
    复制代码

    我们把this对象赋值给了that变量。定义了闭包之后闭包也可以访问这个变量。因此,即使在函数返回之后,that也仍引用这object,所以调用object.getNameFunc()()就返回 “My Object”了。

      

    总结

     

    当在函数内部定义了其他函数,就创建了闭包。闭包有权访问包含函数内部的所有变量。

      闭包的作用域包含着它自己的作用域、包含函数的作用域和全局作用域。

      当函数返回一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。

      使用闭包必须维护额外的作用域,所有过度使用它们可能会占用大量的内存

     

    闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术。下来对其进行一个小小的总结

     

    什么是闭包?

     

    官方说法:

      闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量------《javascript高级程序设计第三版》

     

    下面就是一个简单的闭包:

    <span style="font-family: 'Microsoft YaHei';">function A(){
        var text="hello world";
        function B(){
           console.log(text);
        }
        return B;
    }
    var c=A();
    c(); // hello world </span>

    按照字面量的意思是:函数B有权访问函数A作用域中的变量(text),通过另一个函数C来访问这个函数的局部变量text。因此函数B形成了一个闭包。也可以说C是一个闭包,因为C执行的实际是函数B。

    这个需要注意的是,直接执行A();是没有任何反应的。因为return B没有执行,除非是return B();

    闭包的特性

     

     闭包有三个特性:

     1.函数嵌套函数
    2.函数内部可以引用外部的参数和变量
    3.参数和变量不会被垃圾回收机制回收

    解释一下第3点,为什么闭包的参数和变量不会被垃圾回收机制回收呢?

    首先我们先了解一下javascript的垃圾回收原理:

    (1)、在javascript中,如果一个对象不再被引用,那么这个对象就会被GC(garbage collection)回收; 

    (2)、如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。

      上面的示例代码中A是B的父函数,而B被赋给了一个全局变量C(全局变量的生命周期直至浏览器卸载页面才会结束),这导致B始终在内存中,而B的存在依赖于A,因此A也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

    闭包的作用:

     

      其实闭包的作用也是有闭包的特性决定的,根据上面的闭包特性,闭包的作用如下:

      1、可以读取函数内部的变量,而不是定义一起全局变量,避免污染环境

      2、让这些变量的值始终保持在内存中。

    闭包的代码示例

     

    下面主要介绍几种常见的闭包,并进行解析:

     

    demo1 局部变量的累加。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <span style="font-family: 'Microsoft YaHei';">function countFn(){
        var count=1;
        return function(){             //函数嵌套函数
            count++;
            console.log(count);
        }
    }
    var y = countFn();    //外部函数赋给变量y;
    y();   //2      //y函数调用一次,结果为2,相当于countFn()()
    y();   //3     //y函数调用第二次,结果为3,因为上一次调用的count还保存在内存中,没有被销毁,所以实现了累加
    y=null;  //垃圾回收,释放内存
    y();  // y is not a function
    </span>

    由于第一次执行完,变量count还保存在内存中,所以不会被回收,以致于第二次执行的时候可以对上次的值就行累加。当引入y=null时,销毁引用,释放内存

     

    demo2 循环中使用闭包


    代码如下(下面的三个代码示例):我们的目的是想在每次循环中调用循环序号:

     

    demo2-1

    复制代码
    for (var i = 0; i < 10; i++) {
        var a = function(){
            console.log(i)
        }
        a()  //依次为0--9
    }
    复制代码

    这个例子的结果是没有题的,我们依次打印出了0-9

    每一层匿名函数和变量i都组成了一个闭包,但是这样在循环中并没有问题,因为函数在循环体中立即被执行了

     

    demo2-2

    但是在setTimeout中就不一样了

    1
    2
    3
    4
    5
    6
    <span style="font-family: 'Microsoft YaHei';">for(var i = 0; i < 10; i++) {
        setTimeout(function() {
            console.log(i);  //10次10
        }, 1000);
    }
    </span>

    我们期望的依次是打印出0--10,实际情况是打印出 10次10。即使吧setTimeout的时间改为0,也是打印出10个10。这是为什么呢?

        这是因为setTimeout的一种机制,setTimeout是从任务队列结束的时候开始计时的,如果前面有进程没有结束,那么它就等到它结束再开始计时。在这里,任务队列就是它自己所在的循环。

    循环结束setTimeout才开始计时,所以无论如何,setTimeout里面的i都是最后一次循环的 i。该代码中,最后的 i 为10,所以打印出了10个10.

        这也就是为什么setTimeout的回调不是每次取循环时的值,而取最后一次的值

     

    demo2-3

    解决上面的setTimeout不能依次打印出循环的问题

    复制代码
    for(var i=0;i<10;i++){
        var a=function(e){
            return function(){
    
                console.log(e); //依次输入0--9
            }
        }
        setTimeout(a(i),0);
    }
    复制代码

    因为setTimeout第一个参数需要一个函数,所以返回一个函数给它,返回的同时把 i 作为参数传进去,通过形参 e 缓存了i,也就是说e变量相当于是 i 的一个拷贝 ,并带进返回的函数里面。

     setTimeout 的执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

     

    也可以用下面的写法,和上面类似:

    复制代码
    for(var i = 0; i < 10; i++) {
        (function(e) {
            setTimeout(function() {
                console.log(e);  //依次打印出0-9
            }, 0);
        })(i);
    }
    复制代码

     

     demo3 循环中添加事件

     看下面的一个典型的demo.

    我们希望每次点击li的时候,alert出li的索引值,所以用下面的代码:

    复制代码
     <ul id="test">
        <li>第一个</li>
        <li>第二个</li>
        <li>第三个</li>
        <li>第四个</li>
    </ul>
    
    var nodes = document.getElementsByTagName("li");
    for(i = 0,len=nodes.length;i<len;i++){
        nodes[i].onclick = function(){
            alert(i);   //值全是4
        };
    }
    复制代码

    事与愿违,无论点击哪一个li,都是alert(4),也就是都是alert循环结束之后的索引值。这是为什么呢?

        这是因为循环中为不同的元素绑定事件,事件回调函数里如果调用了跟循环相关的变量,则这个变量取循环的最后一个值。

        由于绑定的回调函数是一个匿名函数,所以上面的代码中, 这个匿名函数是一个闭包,携带的作用域为外层作用域(也就是for里面的作用域),当事件触发的时候,作用域中的变量已经随着循环走到最后了。

        还有一点就是,事件是需要触发的,而绝大多数情况下,触发的时候循环已经结束了,所以循环相关的变量就是最后一次的取值。

     

    要实现点击li,alert出li的索引值,需要将上面的代码进行以下的修改:

    复制代码
    <ul id="test">
        <li>第一个</li>
        <li>第二个</li>
        <li>第三个</li>
        <li>第四个</li>
    </ul>
    var nodes=document.getElementsByTagName("li");
    for(var i=0;i<nodes.length;i++){
        (function(e){
            nodes[i].onclick=function(){
                alert(e);
            };
        })(i)
    }
    复制代码

       解决思路: 增加若干个对应的闭包域空间(这里采用的是匿名函数),专门用来存储原先需要引用的内容(下标)。

       当立即执行函数执行的时候,e 值不会被销毁,因为它的里面有个匿名函数(也可以说是因为闭包的存在,所以变量不会被销毁)。执行后,e 值 与全局变量 i 的联系就切断了,

    也就是说,执行的时候,传进的 i 是多少,立即执行函数的 e 就是多少,但是 e 值不会消失,因为匿名函数的存在。 

     

    也可以用下面的解法,原理是一样的:

    复制代码
    <ul id="test">
        <li>第一个</li>
        <li>第二个</li>
        <li>第三个</li>
        <li>第四个</li>
    </ul>
    
    var nodes=document.getElementsByTagName('li');
    for(var i = 0; i<nodes.length;i++){
        (function(){
           var temp = i;
            nodes[i].onclick = function () {
                alert(temp);
            }
        })();
    }
    复制代码

     

     

    注意事项

     

      1、造成内存泄露

        由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以只有在绝对必要时再考虑使用闭包。

      2、在闭包中使用this也可能会导致一些问题。

           代码示例:来源于《js高级程序设计3》;

    其实我们的目的是想alert出object里面的name

    复制代码
     var name="The Window";
     var object={
         name:"My Object",
         getNameFunc:function(){
             return function(){
                 return this.name;
             }
         }
     }
     alert(object.getNameFunc()()); // The Window
    复制代码

     因为在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。

    每个函数在被调用时,都会自动取的两个特殊变量:this和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止。也就是说,里面的return function只会搜索

    到全局的this就停止继续搜索了。因为它永远不可能直接访问外部函数中的这两个变量。

     

    稍作修改,把外部作用域中的this对象保存在一个闭包能够访问的变量里。这样就可以让闭包访问该对象了。

    复制代码
     var name="The Window";
     var object={
         name:"My Object",
         getNameFunc:function(){
             var that=this;
             return function(){
                 return that.name;
             }
         }
     }
     alert(object.getNameFunc()()); // My Object
    复制代码

    我们把this对象赋值给了that变量。定义了闭包之后闭包也可以访问这个变量。因此,即使在函数返回之后,that也仍引用这object,所以调用object.getNameFunc()()就返回 “My Object”了。

      

    总结

     

    当在函数内部定义了其他函数,就创建了闭包。闭包有权访问包含函数内部的所有变量。

      闭包的作用域包含着它自己的作用域、包含函数的作用域和全局作用域。

      当函数返回一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。

      使用闭包必须维护额外的作用域,所有过度使用它们可能会占用大量的内存

     

    有误之处,欢迎指出

  • 相关阅读:
    转载《XAMPP安装和使用教程》(转)
    EA经典教程(转)
    something to note
    XAMPP安装说明及操作指南 (转)
    Use vCard Objects in RDF/XML (转)
    word2007的相关操作如删除标记区等 及firefox 修改颜色
    jsp和servlet重定向
    HowToMakeCustomSearch
    关于树型dropdownlist的绑定
    在ASP.NET中显示进度条
  • 原文地址:https://www.cnblogs.com/Arnold2015/p/5910080.html
Copyright © 2011-2022 走看看