JavaScript闭包最简洁实用讲解

         在学习JS的路上,无论是实际应用还是面试,闭包肯定是一个绕不过去的技术点。在我的学习生涯中,闭包的难度其实不大,但因其涉及的技术点和应用比较多,理解起

来总有一种“管中窥豹”未见其真身的感觉。读了廖雪峰大大的一篇博客,结合高程的闭包篇章和我自己的理解,给自己梳理一下:  


    一、什么是闭包?特点是什么
         

         众所周知,一般来说由于JavaScript的作用域链限制一个对象只能调用自己链上的对象(不借助bind和call)
         1、自有活动对象 2、全局变量对象  
         如果1和2都没有,则会报错
         

         而闭包特殊之处在于,除了拥有正常的作用域链外,它还可以调用另一个特殊函数的活动对象:
         1、自有活动对象 2、另一个函数活动对象 3、全局变量对象
         一言以蔽之:闭包是一种可以访问其他函数活动对象的函数。
         一个典型的闭包例子:(来自廖雪峰博客)
        

       

 function lazy_sum(arr) {
           var sum = function () {
              return arr.reduce(function (x, y) {
                  return x + y;
              });
          }
               return sum;
        } 
  
       var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()


     

       当我们传入一个数组作为参数调用lazy_sum函数后,与以往不同的是,lazy_sum函数返回的并不是一个具体值而是另一个函数sum。所以f是一个指向sum函数的指针,
当调用结束后,lazy_sum函数的作用域链被消毁,但是sum函数的作用域链环境并不会随之消失,而是随时存在于内存当中,等待调用。
         

       必要时,我们可以用f()去调用这个函数,取得值为数值15。
       

      这个sum函数就是一个闭包,它最大的特点是滞留,即不随调用函数的作用域链销毁而消失。


  二、简单的功能,你不一定非要使用闭包
     

    闭包的好处很多,但最大的坏处是:吃内存。因为上面提到的不消失特性,闭包函数非常占用内存,过度使用闭包可能造成内存占用过大。JS引擎可以使用回收垃圾机制清

理闭包,但还是谨慎为妙。
          

    当你不需要一个闭包再服务时可以使用手动方法释放内存,以上面那个例子为例:
         

       

  let f = lazy_sum([1,2,3,4,5])
          let num = f()
          //使闭包指针指向null即可
          f = null  


  

三、闭包的经典应用


     1、闭包计时器(阐述闭包语法)

        

     

 function create_counter(initial) {
            var x = initial || 0;
                return {
                    inc: function () {
                        x += 1;            
                        return x;       
                        }   
                }
        }
         

         这次的闭包是create_counter函数的局部变量x。每次调用后,它接收最初传入的参数后,被当作闭包函数返回给调用者,即便是create_counter调用结束后也不会消失。
         调用后就是一个程序之中不会断电的计时器,例如

         

 var c1 = create_counter();
          c1.inc(); // 1
          c1.inc(); // 2
          c1.inc(); // 3


          var c2  = create_counter(10);
          c2.inc(); // 11
          c2.inc(); // 12
          c2.inc(); // 13


          2、模拟面向对象

 
         面向对象是一种思想而不是一种具体的技术。实际编程中需要用到面向对象的地方实在是太多。JS没有class机制创造对象,但闭包恰好是JS实现创造面向对象编程的技术。
      

       

 function Person(){
           let name = 'default';
           return{
               getName:function(){
                      return name;
                 },
               setName:function(newName){
                     name = newName
                 },
            }  
 
       };
       let p1 = Person()
       p1.setName('小王') 
       alert(p1.getName()) // 小王


       
       let p2 = Person()
       p2.setName('小张') 
       alert(p2.getName()) // 小张


       通过这段程序可以看到,每当一个Person函数被调用后,即创造了一个实例,有点像JAVA之中的new关键字创造出的对象,它们的属性功能相同,但并不再指向同一个对象,证据就是两次alert()展示的输出数据之中,分别存在了一个name变量保存了‘小王’和‘小张’,它们是独立存在的,真正在JS之中实现了面向对象编程。
          
       在后续的程序之中,每当我们需要创造人物时候就可以let x = Person() 创造出一个新的Person实例,保存一个特有独立的对象供我们使用。为什么说一定要懂闭包,使用JS不会闭包相当于使用JAVA不会创建对象一样。


  四、闭包对变量的取值,一个不能算BUG的BUG
     

      在闭包中,闭包函数只能取得包含函数中任何变量的最后一个值:
          
     

 function createFunctions(){
        let resual = new Array();
        for(let i =0;i<10;i++){
            result[i] = function(){
               return i;
            };
        }
        return result;
     }
     let result = createFunctions()
     alert(result[0])...alert(result[9]) 


     
     按照直觉,似乎result元素的值应该分别是:0,1,...,9
     但实际上,每个元素都是10。
     
     本质上,闭包函数在返回值时,并不是立即执行的,不是说每次执行到resual[i] = function(){return i }
     的时候就返回脚标值,它是在等10个函数全部调用执行之后才开始赋值。此时闭包函数调用的变量i===10,所以它读取了这个值然后赋在每个元素之中。
  
     记得,在闭包之中使用循环会吃大亏,除非你创造另一个匿名函数并立即调用执行它才会纠正这个BUG,这个方法称之为:闭包的闭包
     
   
 function createFunctions(){
         let resual = new Array();
           for(let i =0;i<10;i++){
             result[i] = function(n){
               return function(){
                 return n;
               };
             }(i);
          }
        return result;
     }


     
     让我们理一下思路,这样写,一层闭包是一个参数为n的匿名函数,它在每次执行时会创造二层闭包:是一个返回一层闭包参数n的闭包函数。由于使用了匿名函数立即执行
的(i)语法,一层闭包执行后创造的二层闭包会立刻返回传入的参数n(同时也是脚标值),二层闭包把这个值最终传入数组元素之中,这样一来每个数组元素都拥有独立的数据了。
     
     正是有了这个二层闭包才使得循环的数据i避免了被匿名函数返回的BUG。
  

 五、闭包的误认


        很多人把闭包和匿名函数搞混。
        

        例如一个生成UI的匿名函数:
     

  let datamodel = {
           table:[],
           tree:{},
       };
 
      (function)(dm){
         for(let i=0;i<dm.table.rows;i++){
            let row = dm.table.rows[i];
            for(let j=0;j<row.cells;i++){
                drawCell(i,j)
           }
       }
   })(datamodel)




       它自动执行并无法被访问,但没有返回什么函数,所以并不算是闭包。
       再如:
 
   let person={
           name:'anna',
           showName:function(){
               alert(this.name)
           },
           waitShowName:function(){
             setTimeout(this.showName.bind(this),1000);
           }
       }
       person.waitShowName() //1s后显示 'anna'


       很多人会把这样的调用看作闭包,看起来也确实是waitShowName函数包含了一个setTimeout函数。但是请再回顾一下闭包的定义:
       

       闭包是一种可以访问其他函数活动对象的函数。
       

       这个waitShowName函数指针只不过是person对象的一个方法,它能够调用showName完全是因为使用了this传递了调用对象person,因为它们同属person的活动对象。
并非因为它是闭包。也就是说
        
       是依靠person.waitShowName()传来的this对象调用了showName方法,它本身完全不能调用person的任何属性,所以它不是一个闭包。
       但如果改成这样:
       
     
     

  function person(){
         name = 'anna'
         showName:function(){
               alert(name)
              },
          return{
               waitShowName:function(){
               setTimeout(showName,1000);
            }
          }
        }
       person().waitShowName() //1s后显示 'anna'



       此时的waitShowName()函数则是一个货真价实的闭包函数了,它调用了person对象之中的showName对象,真正的访问了其他函数的活动对象。
        


       如果觉得有启发,请给我点个♥吧!!!

 

猜你喜欢

转载自blog.csdn.net/weixin_40564006/article/details/78242090