JavacScript

Javascript 闭包

2017-12-01  本文已影响0人  羊烊羴

如果要了解闭包,我们需要先了解闭包的由来,闭包的产生,源于JS的词法作用域

词法作用域

作用域是指一个 变量能够访问到的区域 例如我们设置一个var n=0;实际分两步,声明和赋值,var n;n=1;在js中只有函数能够限定作用域,除此之外声明的都是全局作用域,在ES6的新标准中,采用了let和const来限定局部变量,注意局部变量的优先级是高于全局变量的

执行环境是JavaScript中最为重要的一个概念,在js代码开始执行的时候,会创建一个匿名函数的执行环境,执行环境定义了变量或函数有权访问的其它数据,每个执行环境都有一个与之相关的变量对象,环境中定义的所有变量和函数都保存在这个对象中,虽然我们编写的代码无法访问这个对象,但解析器咋及处理数据的时候会在后台使用它

变量的作用域是在定义的时候决定的而不是在执行的时候决定的,变量只能在本层作用域和上层作用域生效,不能在下层作用域生效

举一个栗子来加深理解

    var num;
    function f1(){
        num=3;
    }
    f1();
    console.log(num);//3

在这个栗子中,num的值在全局最开始设置的是undefined,但是在函数f1中改变了该值,这个值变为3,在函数f1执行了一次之后我们再打印num的值,得到的是num=3,通过解读JavaScript是词法作用域我们可以知道,num这个变量的作用域是在它定义的时候决定了它是一个全局变量,所以即使在函数中执行num的操作,我们在函数外部依然可以获取到这个值

注意一个细节,获取全局变量比获取局变量要耗费性能,举个例子:

    function (obj){
        var i=0,
            l=obj.length;
        for(;i<l;i++){}
    }

在这段代码里,我们获取设置局部变量obj.length用来进行循环条件判断,使用i<l是一个比i<obj.length更佳的选择,因为如果obj包含大量的代码,我们在循环时每一次都要重新获取obj的值,而设置l则可以降低这部分的性能损耗

闭包

在JS中只有function能形成块级作用域,在函数内部的变量外部是无法获取的,而在函数中嵌套函数,被嵌套的函数则可以拿到外层函数的变量的值,所以有的时候我们需要拿到一个函数的内部数据时,可以采用如下的方法:

    function A(){
        var name="tom"

        function B(){

            return name
        }

        return B();

    }

    console.log(A());//tom

以上的代码就是我们常说的闭包,官方的闭包的解释十分的概念化,我对闭包的理解就是:

  1. 初步的理解:能够获取其他函数的内部数据的函数
  2. 深入的理解:函数记住并访问其所在的词法作用域,叫作闭包现象,而此时函数对作用域的引用叫作闭包(闭包就是引用,维基上有一段对闭包的引用解释的我觉的是比较好:引用了自由变量的函数,自由变量和函数将一同存在),如果想要理解这一块,我们需要对JavaScript中的垃圾回收机制JavaScript的存储机制做一些了解
JavaScript的垃圾回收机制

垃圾回收的主要工作是跟踪内存的分配和使用,在内存不再工作时,将其进行释放

在垃圾回收机制中,涉及到的算法比较多,所以也不打算深入,只做简单的了解即可,我们只需要知道:

在js中,在创建变量或函数时,会开辟空间,有一个属性来标注它们是否被其它变量,被引用则计数器++,

  1. 如果函数被调用过了,并且以后不会再用到,此时判断计数器为0,那么垃圾回收机制就会将其作用域进行销毁
  2. 全局变量是不会被销毁的
JavaScript的存储机制

我们都知道,在JS的存储中是分为堆存储和栈存储的,堆存储是用来存储对象,也就是引用数据类型,而栈存储是用来存储简单数据类型,在这里我们需要了解的是:

  1. 对象的引用是通过地址来传递的,一个对象应用另一个对象后只是它的地址指向发生了改变,而它原来的值是依然存在的
  2. 简单数据类型的引用是直接在栈中把原先数据进行替换的

了解了以上两个概念我们可以再回头看一下关于深入的理解闭包,如果一个函数被引用,那么它的作用域就不会被垃圾回收机制进行销毁,这就可以理解为记住了这个作用域,如果对其内部的变量进行访问,那么称为访问

<script type="text/javascript" language="javascript">
    function a(){
        var i=0;
        return function b() {
            alert(++i);//记住并访问
        }
    }
    c=a();//引用
    c();
</script>

由于比包内的函数对外部函数的变量进行了引用,在垃圾回收机制看来,引用计数不为0,所以在函数的作用域被销毁后,闭包内的变量会一直存在,这也是闭包的缺点,大量的闭包引用如果在引用后没有赋值为null会占用大量的内存空间,在IE低版本中会导致内存泄漏

闭包还有一个特性是闭包只能气的包含函数中任何变量的最后一个值,例如一些常见的面试题中经常出现这个问题

    function person(){

        var arr=[];

        for(var i=0;i<10;i++){
           arr[i]=function (){
                return i;
            };
        };
        return arr;
    }
    
    console.log(person()[1]());//10

在上面的代码中arr的每个位置存储的都是10,如果我们想要使每个返回不同的数字那么我们需要对代码进行改进

    function person(){

        var arr=[];

        for(var i=0;i<10;i++){
           arr[i]=function (num){
                return function(){//再次添加一个闭包,在这个闭包内将每次i的值进行存储
                    return num;
                };
            }(i)
        };
        return arr;
    }
    console.log(person()[1]());//10

闭包的用途

  1. 获取其它函数内部的变量的值

  2. 缓存变量的值

        function A(){
            var i=1;
            return function(){
                i++;
                console.log(i);
            }
        }
        var a=A();
        a();
        a();
        a();
      //这里我们要探讨一个问题,闭包缓存数据的形式是什么,之前一直对闭包的概念有误解,一直以为A事必报,但是实际上
      // return function(){
      //          i++;
      //          console.log(i);
      //     }
      //这部分才是真正的闭包,所以能够进行缓存的是这一部分,这也就解决了之前的一个疑惑,为什么需要写
      // var a=A();这一步再调用a();才能看到闭包缓存的数据
      //我们可以根据这个特性座椅计时器,来记录一个构造函数创建了多少个对象
       var C=function(){
    
            var i=0;
    
            return function(){
    
                return ++i
            }
        }();//让函数自调用自身
    
        function fn(name){
    
            if(this==window){//判断当前调用的是不是window对象
                return 
            }
    
            this.name=name;
    
            fn.cn=C();//注意,在这里实际上已经相当于直接使用++i为fn.cn赋值了
        }
    
        var a=new fn("tom");
        var b=new fn("son");
        var c=new fn("xm");
    
        console.log(fn.cn)//3
    
  3. 设置变量的可读可写(封装)

        function A(_name){
            var name=_name;
    
            return {
                getName:function(){//可读
                    return name;
                },
                setName:function(newName){//可写
                    name=newName; 
                }   
    
            }
        }
    
        var a=new A("tom");
        console.log(a.getName())//tom
        a.setName("xm");
        console.log(a.getName())//xm
      
      //还有一个写法
      
        function A(_name,_age){
            var name=_name,
                age=_age;
    
            return {
                name:function(value){
                    if(value===undefined) return name;
                    else name=value;
                },
                age:function(value){
                    if(value===undefined) return age;
                    else age=value;
                }
            } 
        }
        
        var a=new A("tom",18);
        console.log(a.name());//tom 
        a.name("xm");
        console.log(a.name());//xm
    
  4. 沙箱模式

    (function(){})();//防止代码污染
    //这里我们需要提到一个概念,也就是JS中的依赖注入,和AngularJS中的类似的概念,JS中的依赖注入也就是表示,在JS中如果需要调用某种我们已经封装好的,或者外部的JS库,那么我们可以提前声明
    1.在JS中,如果让JS自己解析,寻找我们定义的变量,这个过程的效率很低,无形中会损耗一部分性能,如果我们以实参的方式传入,那么会减少这部分额性能损耗
    (function(window){
    
      })(window)
    2.提前声明我们使用了什么库,便于代码的维护和可读性,保障代码的健壮性
    (function($){
    
      })($)
    
  5. 提前返回

      var addEventlistner=function (){
       if(window.addEventlistner){
    
        return function(elem,type,calback,bool){
    
          elem.addEventlistner("type",calback,bool);
        };
      }else{
          return function(elem,type,calback){
    
            elem.attachEvent("on"+type, calback)
        };
      }
    }();
     //在之前我们进行能力检测的时候,每次调用都会进行if..else..的判断,其实在我们进入浏览器的时候,浏览器的能力已经确定了,所以我们可以通过利用闭包的提前返回的特性,把能力检测的结果保存下来,不需要每次都进行判断
    
上一篇下一篇

猜你喜欢

热点阅读