【詳解】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);
            }
        }

  而有時候,咱們想要一直使用到的變量,也能夠用定義爲全局變量的方式來達到不被回收的目的,可是相比起閉包而言,全局變量還有一個更大的缺點,就是全局污染,當你隨意地定義全局變量來容納你應用的全部資源時,你的程序和其餘應用程序、組件或類庫之間發生衝突的可能性就會顯著升高,這種時候使用閉包來隱藏信息,是一個有效的方法。

相關文章
相關標籤/搜索