【详解】JS中的做用域、闭包和回收机制

  在讲解主要内容以前,咱们先来看看JS的解析顺序,咱们惯性地以为JS是从上往下执行的,因此咱们要用一个变量来首先声明它,来看下面这段代码:javascript

  alert(a);
  var a = 1;

  你们以为这段代码有什么问题吗?会报错吧,a变量没有声明就调用?或者,可能弹出数字1?html

  实际上,你会发现这段代码执行的结果是弹出一个“undefined”,为何会这样呢?下面咱们就来说讲JS的解析顺序。java

一、JS的解析顺序

  其实说JS是自上而下解析是正确的,可是是分为两步:编程

  1. 先自上而下解析声明,包括用var、function声明的变量和函数,以及函数的参数的声明(隐式声明)。这里要注意解析声明并不会赋值,好比你写了var a = 1;在这一步只会解析var a。这一步被称为预解析,在做用域内声明的变量会被提高到做用域的顶部,且对其赋值undefined这个过程称之为变量提高;而在做用域内的函数定义会被提高到做用域的顶部,其值为函数自己,这个过程称之为函数提高。
  2. 再自上而下执行,包括赋值、判断、循环、函数调用等等。这里要注意经过function声明的部分是直接跳过的,由于这部分属于声明,而不是执行代码,只有当这个函数被调用的时候,函数内部的代码才会被解析,而事件类函数是要事件触发的时候才会执行。

  咱们来看下面这个案例:闭包

alert(a);
var a = 123; function a(){ alert('a1'); } function a(){ alert('a2'); } alert(a); a();

  解析分析:编程语言

       step1:解析声明var a、function a(){alert('a1');}、function a(){alert('a2')},这里由于开始声明的变量a和后面声明的两个函数a重名了,而JS中函数的声明优先于变量的声明(即便变量声明在后,函数声明在前,a也依然是函数),因此第3行的函数声明会覆盖第2行的变量声明,而第6行又声明了一个同名函数,此时后面声明会覆盖前面声明,故a会变为第6行声明的函数。函数

    step2:执行第1行alert(a),这里弹出第6行声明的函数体function a(){alert('a2');};而后执行第2行a=123,这里a从函数又变成了数字123;而后执行第9行的alert(a),这里弹出123;最后执行a(),此时a是数字不是函数,这个调用是有语法错误的,故会报错。this

  以上是JS解析的基本规则,为了能熟练运用这个规则,咱们还须要了解一下JS中的做用域,本文将用大量的案例来说解做用域和闭包的知识。spa

二、JS的做用域

  在ES5中做用域分为两种:code

  1. 全局做用域
  2. 局部做用域

  直接定义在script标签下的变量和函数都在同一个做用域——全局做用域,在全局做用域里定义的变量和函数,分别被称为全局变量和全局函数,它们在函数做用域里也是可以被访问的。

  在某个函数或者对象的内部定义的变量和函数所在的做用域为局部做用域,这部分变量和函数只在函数或对象的内部有效,将不能在函数或对象外直接访问(只能间接访问)。在ES6中还会有块级做用域,即任何用{ }包含起来的代码块都为一个块级做用域,在本文中只讲ES5中的全局和局部做用域。

特性:

  除了父级的this和arguments这两个特殊的对象,局部做用域能够访问父级和全局做用域里的变量和函数;父级和全局不能直接访问局部的变量和函数。当局部声明的变量(或函数)与父级或者全局的变量(或函数)名字相同时,局部优先使用本身内部声明的。

  接下来咱们一块儿看几个例子:

案例1:

    fn();
    alert(a);
    var a = 0;
    alert(a);
    function fn() {
        var a=1;
    }

  解析分析:首先看全局做用域

  step 1:解析声明 var a、function fn(){}。

  step 2:执行 fn();----------函数局部做用域:step1:解析声明 var a;    

                       step2:执行a=1; 注意这里的a是函数内部的a,不是全局的a。

        alert(a);-----在全局找到var a的声明,a未赋值,弹出undefined。

        a=0;----------给全局变量a赋值0。

        alert(a);------弹出0。

案例2:

    fn();
    alert(a);
    var a = 0;
    alert(a);    
    function fn() {
        a = 1;  //跟案例1仅此处不一样
    }

  解析分析:先看全局,局部直接跳过,调用的时候再看。

  step1:解析声明:var a、function fn(){}。

  step2:执行:fn();----------step1:查找声明,没有任何声明。

              step2:执行a=1,这里函数内部没有声明变量a,会往上看父级有没有,父级(全局)有变量a,这里直接给这个a赋值。

        alert(a);-------弹出1,由于这个全局变量,在执行fn()函数的时候被赋值为1。

        a=0;------------将a的值改成0。

        alert(a);--------弹出0。

案例3:

    var a = 0;

    !function(a){
     alert(a)
    }()

  解析分析:注意函数参数的隐式声明。

  step1:解析声明var a、function(){}。

  step2:执行a=0。

       function(){}这个匿名函数的自执行-------step1:解析声明var a,这里是形参a的声明。  

                          step2:执行alert(a),弹出undefined。

  注意这里弹出undefined,是由于自执行的传参的()里也没有传任何参数,且函数内部在alert(a)以前也未给a赋值,故a为undefined。

案例4:

    function fn(a){
        var a =0;
        alert(a);
    }
    fn(3);

  解析分析:函数形参的声明是在函数的第一行代码以前,调用函数时传入的实参赋值给形参也是在执行的第一步。

  step1:解析声明function fn(){}

  step2:执行fn(3)-------step1:声明形参var a,注意第一行代码的var a声明是多余的。

            step2:执行a=3,先将传入的实参数3赋值给形参a。

                  a=0,改变参数a的值为0。

                  alert(a),弹出0。

案例5:

    var a = 1;
    function fn() {
        var b = 5;
        return function () {
            b++;
            alert(b)
        }
    }
    fn()();

  解析分析:函数的做用域在函数定义时就决定了它的位置,而不是在执行的时候决定的,只不过这个做用域在执行时,才生效。

  step1:解析声明:var a、function fn(){}。

  step2:执行:a=1。

        fn()----------step1:声明:var b、function(){}

             step2:执行:b=5; return function(){}。

        fn()()-------至关于执行的是上一步return返回的匿名函数。

             step1:声明:没有声明。

             step2:执行:b++,b这个变量本身没有,往父级fn里找,能够找到变量b,其值为5,这里++,变为6。

                    alert(b),弹出6;

案例6:

    fn()();
    var a = 0;
    function fn() {
        alert(a);
        var a = 3;
        function c() {
            alert(a);
        }
        return c;
    }

  解析分析:

  step1:解析声明:var a、function fn(){}。

  step2:执行:fn()-------step1:声明var a、function c(){}。

              step2:执行alert(a),先在本身的做用域找有没有a,有a未赋值,弹出undefined。

                  a=3,给本身的a赋值3。

                  return c,返回c这个函数。

·        fn()()-----至关于执行c函数。step1:解析声明,没有任何声明。

                     step2:执行alert(a),本身没有a,往上父级有a,弹出父级a的值3。

        a=0,给全局变量a赋值0。

案例7:

    function fn() {
        var a;
        alert(a);
        if(true){
           var a = 1;
        }
        alert(a);
    }
    fn();

  解析分析:ES5中if、for、while、switch等的{ }不算单独的做用域。

  step1:解析声明:function fn(){}。

  step2:执行:fn()-------step1:声明var a。

              step2:执行alert(a),弹出undefined。

                  if(true),if判断为真,下一步执行if内部代码

                  a=1,注意if里虽然有var a,但属于重复声明,这个a就是fn函数体一开始声明的那个a。

                  alert(a),弹出1。

案例8:

     var y = 1;
    if(function(){}){
        y += typeof f;
    }
    console.log(y);

  解析分析:JS中有六种状况为假:“ ”,0,NaN,undefined,null,false。当一个变量未声明就直接用typeof得到它的类型时,typeof会返回‘undefined’。

  step1:解析声明:var y、function(){}。

  ste2:执行:y=1。

        if判断:先将function(){}隐式转换为字符串,字符串不为空,再转换为true。

        if内部:y += typeof f,等价于y = y + typeof f,先看等号右边f是一个未声明的变量,默认声明,typeof f 为undefined。

                  当+号的两边不都是数字的时候,会实现拼接,故获得1undefined,赋值给y。

        console.log(y),控制台打印出1undefined。

案例9:

    var foo = 1;
    function bar(){
        if(!foo){
            var foo = 10;
        }
        alert(foo);
    }
    bar();

  解析分析:

  step1:解析声明:var foo、function bar(){}。

  step2:执行foo=1。

        bar()-------step1:声明var foo。

            step2:执行if判断,foo是函数做用域里声明的foo,此时为undefined,会转换为false,故!foo为true。

                  if内部,foo=10。

                 alert(foo),弹出10。

特殊案例:

    var a = 5;
    function fn() {
        var a = 10;
        alert(a)
        function b() {
            a++;
            alert(a)
        }
        return b;
    }
    var c = fn();
    c();
    fn()();
    c();

  解析分析:

  step1:解析声明:var a、function fn(){}、var c。

  step2:执行:①a=5。

        ②c=fn(),先执行fn()-----step1:声明var a、function b(){}。

                  step2:执行a = 10。

                        alert(a),弹出10。

                        return b。

             将fn()执行的结果赋值给c,此时c=b。

        ③c(),至关于执行b()------step1:没有声明。

                    step2:a++,本身没有a,找父级fn要,父级a=10,此时a=11。

                      alert(a),弹出11。

        ④fn()(),先执行fn()-----step1:声明var a、function b(){}。

                  step2:执行a = 10。

                        alert(a),弹出10。

                        return b。

            再执行fn()(),至关于执行b()------step1:没有声明。

                            step2:a++,本身没有a,找父级fn要,父级a=10,此时a=11。

                              alert(a),弹出11。

        ⑤c(),至关于执行b()?------step1:没有声明。

                      step2:a++,本身没有a,找父级fn要,父级a=10,此时a=11?

                        alert(a),弹出11?实际运行的时候咱们会发现这里弹出的是12。

  这就是咱们接下来要讲的闭包。

三、JS中的闭包

  在上一节的特殊案例中,函数b中用到了父级做用域的一个变量a,而后咱们将这个函数b赋给了c,当c被调用的时候,变量a的值会保存c这次对其执行的改变,故当咱们第二次调用c的时候,a的值会在11的基础上再加1,若是咱们重复调用c,咱们就能看到a的值每次都在增长。

  这里须要注意的是,同一个函数定义,每被调用执行一次都是在产生一个新的做用域,好比上例中的fn,第一次调用的时候把b赋值给了c,而后在倒数第二行的时候又被调用了一次,但这次产生的做用域和c=fn()()时产生的做用域是不一样的两个,因此倒数第二行的fn()()不会影响到c()中a的值。

  咱们来看看闭包造成的条件:

  1. 函数嵌套函数。
  2. 内部函数使用了外部函数的参数或者变量。

  做用:内部使用到的那个父级的参数或变量,可以被永久保存下来。

案例1:

    function fn() {
        var a = 1;
        return function () {
            alert(++a)
        }
    }
    var fn2 = fn();
    fn2()   //弹出2
    fn2()   //弹出3
    var g = fn(); 
    g(); // 弹出2
    fn2(); // 弹出4
    g(); //弹出3

  这个案例跟上一个案例相似,就不一步一步解析分析了,须要注意的是fn2和g,虽然都等于fn(),可是由于fn这个函数定义每次调用都会产生不一样的做用域,故而fn2和g内部的变量a在是不一样的做用域下,互不影响。而像fn2这样经过表达式被赋值的函数,每次调用都是在同一个做用域。

案例2:来说一个闭包运用的例子

  假设咱们页面上有n个li,咱们要给每一个li注册一个点击事件,点击的时候弹出li的序号,咱们通常会这样写:

    var aLi = document.getElementByTagName('li');
    for(var i = 0 ; i < aLi.length ; i++){
        aLi[i].onclick = function(){
            alert(i);
        }
    }    

  初看以为代码没有问题,可是实际上等咱们运行的时候就会发现点击任何一个li弹出的都是n,这是由于当咱们点击的时候,for循环早已运行完毕,i的值已经增长到n。

  (对上面的现象不明白的初学者能够看这段解释:for循环是给每个li注册点击事件,仅注册而不执行,因此for循环不会等咱们点击了第i个li以后,再执行第i+1次循环,当页面加载的时候,for循环已经瞬间执行完毕了,故i的值已经等于n了,这时候不论咱们点击任何一个li,弹出的值都会是n。)

  闭包可以很好地解决这个问题,咱们来回想一下闭包造成的条件,第一条函数嵌套函数,那么咱们能够在点击函数的外面再包含一个函数;第二条内部函数使用外部函数的参数或者变量,这个参数或者变量会被永久保存下来,那么咱们能够把i做为外面那个函数的参数或者变量,而这里i是在for循环的时候被声明的,那么咱们能够做为外面函数的参数来用。

  var aLi = document.getElementsByTagName("li");
    for(var i=0;i<aLi.length;i++){
        (function (index) {
            aLi[i].onclick=function () {
          alert(index)
       } 
     })(i)//实参 
  }

  这段代码完美地解决了咱们刚才的问题,咱们把for循环拆分来分析一下,以下面的代码:

    var aLi = document.getElementsByTagName("li");
    var n = aLi.length;

   (function (index) {//此处隐含有一句 var index = 0;
            aLi[0].onclick=function () {
         alert(index) 
       } 
  })(0)//实参

    (function (index) {//此处隐含有一句 var index = 1;
            aLi[1].onclick=function () {
         alert(index) 
       } 
   })(1)//实参  

    ......

    (function (index) {
            aLi[n-1].onclick=function () {
         alert(index) 
       } 
   })(n-1)//实参 

  闭包虽然有这么好的优势,但在咱们的实际工做中,咱们会尽可能避免使用它,由于使用的那个变量会被保存不会被释放(除了刷新页面或关闭页面),因为闭包的特性,会对内存的消耗较大。下面咱们来说一下JS的回收机制。

四、JS的回收机制

  在以前的例子中,咱们提到一个函数的定义(经过function定义的函数)在每次调用都会造成一个新的做用域,这个现象使人费解,感受JS必定是作了什么手脚。这就是咱们接下来要讲的JS的回收机制。

  实际上任何一门编程语言都有本身的回收机制,又称为垃圾回收机制,试想若是一个语言没有本身的回收机制会是什么样?那咱们的程序将会由于没有及时回收无用的变量和函数而占据愈来愈多的内存,会使得咱们的程序愈来愈慢。比如咱们的城市,若是没有垃圾处理机制,你们想一想会是什么样?因此回收机制对于一门编程语言来讲相当重要。

  回收机制要工做,首先得有回收的规则,即要明白哪些是要回收的,哪些是不回收的。那么,JS中是如何规定的呢?

  JS中规定变量所在的做用域的生命周期决定了变量的生命周期。故而全局变量是不会被回收的,除非您关闭网页,结束window;而函数内部的变量,则在函数被调用时生效,函数执行结束时会被回收,这就是为何咱们在父级不能直接访问子级变量的缘由,而闭包又会有所不一样。注意生命周期的长短由执行的时候决定,咱们又回到以前那个特殊案例:

    var a = 5;
    function fn() {
        var a = 10;
        alert(a)
        function b() {
            a++;
            alert(a)
        }
        return b;
    }
    var c = fn();
    c();
    fn()();
    c();

  咱们来看看全局都有哪些变量和函数:变量a、c,函数fn,它们是不会被回收的,因此咱们能够在全局去调用它们。

  重点来看看这句代码var c = fn(),首先执行fn(),首先按照咱们的理解,当fn执行完后,它里面的变量和函数都会被回收,所占用的内存都会释放,可是这里,fn执行完毕后返回了一个b函数,这个函数赋值给了c,同时函数中还用到了父级fn的一个变量a,此时由于c是全局变量,其生命周期还未结束,因此JS会为c开辟一个闭包空间用来存储变量a和函数体b,同时回收掉fn里的变量和函数。

  这样在执行下一行代码fn()()的时候,再次调用fn(),由于以前调用fn后,里面的函数和变量等已经回收,因此此次又会从新为fn里面的函数和变量分配空间,产生一个新的做用域,fn()执行完后依然返回一个函数b,此时再执行fn()(),至关于执行b(),执行完毕后,由于全局没有变量引用到b,而b的父级fn函数的声明周期已经结束,因此会回收掉变量a和函数b,因此这行代码看似运用了闭包,其实是一个假的闭包。

  最后一行代码c(),会直接执行以前为c开辟的闭包空间里的b函数体,函数体内部用到的变量a保存了上一次的值,因此此次会在上一次的基础上+1。

  通俗地讲,闭包实际上就是保护变量的一个封闭空间,保护一个即将被释放的变量不被释放,以便下次再用到它。因此它和全局变量同样都比较耗内存,通常咱们会尽可能避免使用它,好比咱们以前在闭包的案例2,咱们其实能够经过给每个li对象一个自定义属性来实现所要的功能:

  

    var aLi = document.getElementsByTagName('li');

        for(var i = 0;i < aLi.length;i++){
            aLi[i].index = i;//自定义属性来存储i
            aLi[i].onclick = function () {
                alert(this.index);
            }
        }

  而有时候,咱们想要一直使用到的变量,也能够用定义为全局变量的方式来达到不被回收的目的,可是相比起闭包而言,全局变量还有一个更大的缺点,就是全局污染,当你随意地定义全局变量来容纳你应用的全部资源时,你的程序和其余应用程序、组件或类库之间发生冲突的可能性就会显著升高,这种时候使用闭包来隐藏信息,是一个有效的方法。

相关文章
相关标签/搜索