閉包----你所不知道的JavaScript系列(4)

1、閉包是什麼?數組

  · 閉包就是可使得函數外部的對象可以獲取函數內部的信息。閉包

  · 閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(一般是一個函數),於是這些變量也是該表達式的一部分。函數

  · 閉包就是一個「捕獲」或「攜帶」了其被生成的環境中、所屬的變量範圍內所引用的全部變量的函數。性能

  還有不少不少解釋......this

 

  函數對象能夠經過做用域鏈互相關聯起來,函數體內部的變量均可以保存在函數做用域內,這叫作「閉包」。      --《JavaScript權威指南》spa

  當函數能夠記住並訪問所在的詞法做用域時, 就產生了閉包, 即便函數是在當前詞法做用域以外執行。   --《你所不知道的JavaScript》

指針

 

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}

var baz = foo();
baz(); // 2     

 

這就是閉包的效果。 函數 bar() 的詞法做用域可以訪問 foo() 的內部做用域。而後咱們將 bar() 函數自己看成一個值類型進行傳遞。在這個例子中,咱們將 bar 所引用的函數對象自己看成返回值。bar()顯然能夠被正常執行。可是在這個例子中,它在本身定義的詞法做用域之外的地方執行。在 foo() 執行後, 一般會期待 foo() 的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器用來釋放再也不使用的內存空間。因爲看上去 foo() 的內容不會再被使用,因此很天然地會考慮對其進行回收。而閉包的「神奇」 之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收。 誰在使用這個內部做用域? 原來是 bar() 自己在使用。拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內部做用域的閉包,使得該做用域可以一直存活,以供 bar() 在以後任什麼時候間進行引用。bar() 依然持有對該做用域的引用, 而這個引用就叫做閉包。
不管使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時均可以觀察到閉包。閉包使得函數能夠繼續訪問定義時的詞法做用域。

code

 

2、做用域鏈和js垃圾回收機制對象

  在深刻理解閉包以前,最好能先理解一下做用域鏈的含義以及js垃圾回收機制。blog

  簡單來講,做用域鏈就是函數在定義的時候建立的(而不是在函數調用時定義),用於尋找使用到的變量的值的一個索引,而他內部的規則是,把函數自身的本地變量放在最前面,把自身的父級函數中的變量放在其次,把再高一級函數中的變量放在更後面,以此類推直至全局對象爲止。當函數中須要查詢一個變量的值的時候,js解釋器會去做用域鏈去查找,從最前面的本地變量中先找,若是沒有找到對應的變量,則到下一級的鏈上找,一旦找到了變量,則再也不繼續。若是找到最後也沒找到須要的變量,則解釋器返回undefined。

  瞭解了做用域鏈,咱們再來看看js的內存回收機制。通常來講,一個函數在執行開始的時候,會給其中定義的變量劃份內存空間保存,以備後面的語句所用,等到函數執行完畢返回了,這些變量就被認爲是無用的了,對應的內存空間也就被回收了。下次再執行此函數的時候,全部的變量又回到最初的狀態,從新賦值使用。可是若是這個函數內部又嵌套了另外一個函數,而這個函數是有可能在外部被調用到的,而且這個內部函數又使用了外部函數的某些變量的話,這種內存回收機制就會出現問題。若是在外部函數返回後,又直接調用了內部函數,那麼內部函數就沒法讀取到他所須要的外部函數中變量的值了。因此js解釋器在遇到函數定義的時候,會自動把函數和他可能使用的變量(包括本地變量和父級和祖先級函數的變量(自由變量))一塊兒保存起來。也就是構建一個閉包,這些變量將不會被內存回收器所回收,只有當內部的函數不可能被調用之後(例如被刪除了,或者沒有了指針),纔會銷燬這個閉包,而沒有任何一個閉包引用的變量纔會被下一次內存回收啓動時所回收。

 

3、閉包的缺點以及優勢

  缺點:

    (1)因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。

    (2)閉包會在父函數外部,改變父函數內部變量的值。因此,若是你把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。

  

  優勢:

    (1)但願一個變量長期駐紮在內存中。
    (2)避免全局變量的污染。
    (3)私有成員的存在。

 

接下來,咱們就針對閉包的三個優勢進行解析,一塊兒來看看吧。

 

儲存變量

function test() {  
   var a = 1;  
   return function(){
       alert(a++)
   };  
}         
var fun = test();  
fun();   // 1  執行後 a++,而後a還在~
fun();   // 2
fun = null;   //解除引用,等待垃圾回收

  在執行fun = test()時,其實就至關於fun = function(){ alert(a++) }。若是咱們平時這樣聲明並賦值一個變量的時候,在調用的時候就會報錯(a未被聲明)。可是在閉包中卻不會報錯,由於在調用test() 的時候,變量a是存在於內部匿名函數的做用域鏈上的,而且在調用完以後變量a不會消失。

緣由:因爲匿名函數一直在引用變量a,js垃圾回收機制將不會把變量a當作垃圾去回收,因此a會一直存在內存當中。

 

既然知道了閉包能將變量一直保存在內存中,那麼咱們再來看看一下幾個例子的區別,來加深對閉包的瞭解。

第一段代碼:

var scope = "global scope";
function checkscope(){
     var scope = "local scope";
     function f(){
          return scope;
     } 
     return f();
} 


checkscope();    //返回值會是什麼?

第二段代碼:

var scope = "global scope";
function checkscope(){
     var scope = "local scope";
     function f(){
          return scope;
     } 
     return f;
} 


checkscope()();    //返回值會是什麼?

  咱們能夠看到第一段代碼,在函數checkscope內返回的函數f的結果,很清楚能夠知道返回值爲"local scope"。而在第二段代碼中,函數checkscope內返回的函數對象f,返回的是一個對象而不是一個運算的結果。如今在定義函數的做用域外面,調用這個嵌套函數f,返回的結果依舊是"local scope"。爲何在定義函數外部調用嵌套函數,返回的結果卻仍是函數內部變量。這裏面就涉及到了函數的做用域鏈。上面說過,函數的做用域鏈是在函數定義時就生成的,而不是在函數調用時生成的。嵌套函數f()定義時在checkscope的做用域鏈內,其中的scope是局部變量,其值爲local scope,無論在什麼時候何地執行函數f(),這種綁定在執行f()時依然有效,所以返回的值是"local scope"。

 

避免全局污染

  在這裏,我先提個問題,若是要讓你實現一個從數字1開始累加的功能,你會怎麼實現?

  咱們來看看下面兩個代碼。

 

//使用全局變量
var a = 1;
function abc(){
    a++;
    alert(a);
}
abc();   //2
abc();   //3

 

//使用局部變量
function abc(){
    var a = 1;
    a++;
    alert(a);
}
abc();   //2                  
abc();   //2

  在上面的例子能夠看出,使用全局變量能夠很輕鬆就實現累加效果,可是使用全局變量會形成全局污染。咱們可使用局部變量來實現累加,可是上面使用局部變量的結果不盡人意(緣由:只是對函數進行簡單的調用,每次調用完以後函數內的變量都會被銷燬。再次調用時會從新賦值初值,並不會保存上一次調用後的值)。那咱們要怎麼才能利用局部變量實現累加呢?

  在前面的例子中,咱們看到了閉包能夠保存函數內的變量,因此咱們能夠利用閉包將變量a的值保存起來,每次調用以後,a的值會累加而且不會被銷燬。來看看下面怎麼利用閉包怎麼實現累加功能的。

function outer(){
    var x=1;
    return function(){                     
        x++;
        alert(x);
    }
}
var y = outer();   //外部函數賦給變量y
y();    //y函數調用一次,結果爲2,至關於outer()()
y();    //y函數調用第二次,結果爲3,實現了累加

  咱們知道,js是沒有塊級做用域的概念的,這裏咱們就能夠看到,能夠用閉包來模擬模擬塊級做用域,從而避免全局污染。

 

私有成員

  因爲閉包能夠捕捉到單個函數調用的局部變量,並將這些局部變量用作私有狀態,因此能夠來定義私有成員。

var init = (function(){
         var counter = 0;
    return function(){
                return counter++;
        }
})();

init();   //0
init();   //1
init();   //2

  上面這段代碼定義了一個當即調用的函數,所以這個函數的返回值賦值給了變量init。再來看一下函數體,這個函數返回另一個函數,因爲被返回的函數可以訪問本身做用域內的變量,並且可以訪問其外部函數(函數體)中定義的counter變量。噹噹即執行函數執行以後,其餘任何代碼都沒法訪問變量counter,只有其內部的函數才能訪問到它。因此此時變量counter就變成一個私有變量,存在於閉包中。

  像counter這樣的私有變量不單隻能夠存在一個單獨的閉包中,在同一個外部函數內定義的多個嵌套函數也能夠訪問到它,這多個嵌套函數都共享一個做用域鏈,如今看一下這段代碼。

function counter(){
     var n = 0;
     return{
         count : function(){ return n++; }
         reset :  function(){ n =0; }
     }
}

var c = counter(), d = counter();
c.count();    //0
d.count();    //0
c.reset();     //reset()和count()方法共享
c.count();    //0  (由於重置了c)
d.count();    //1  (沒有重置d,所以n繼續累加)

若是如今須要實現一個功能:返回一個函數組成的數組,而且它們的返回值分別是0~9。你會怎麼實現?會不會跟下面一段代碼同樣?

function contsfuncs(){
     var funcs = [];
     for(var i = 0; i<10; i++){
           funcs[i] = function(){
           return i;
      }
     }
     return funcs;
}

var funcs = contsfuncs();
funcs[5]();   //返回值是什麼?

  上面這段代碼建立了10個閉包,而且將它們存儲到一個數組中。因爲這些閉包都是在同一個函數調用中定義的,因此他們均可以共享變量i。當contsfuncs()返回時,變量i都是10,因此全部的閉包都共享這一個變量值,所以,數組中的函數的返回值都是同一個值。那咱們要怎麼作才能實現咱們想要的功能呢?

function contsfuncs(v){
     return function(){
          return v;
     };
}
var funcs = [];
for(var i = 0; i<10; i++){
     funcs[i] = contsfuncs(i);
}

funcs[5]();   //5

  因爲外部函數contsfuncs()老是返回一個返回變量v的值,因此在for循環中,因爲每次調用外部函數contsfuncs()時,傳入的v的值是不一樣的,因此所造成的閉包都是不一樣的,這十個閉包中變量v的值分別爲0~9,因此數組funcs中在第五個位置的元素所表示的函數返回值爲5。


總結:
(1)在同一個調用函數內部定義多個閉包,這些閉包共享調用函數的變量,每一個閉包對其操做都會影響到其餘閉包對其引用的值。
(2)利用同一個調用函數在函數外部構造的多個閉包,則這些閉包都是獨立的,擁有本身的做用域鏈,互不干擾。

 

4、閉包中的this

  在閉包中使用this,須要特別當心,由於很容易就出錯。不信?看看下面例子就知道了。

var name = "window";
var obj = {
    name : "object",
    getName : function(){
        return function(){
           return this.name;
        }
    }
};
alert(obj.getName()());   // window

  咱們原本的想法是想調用閉包後返回obj對象中的name的值,即"object",可是結果倒是返回"window"。爲何這個閉包返回的this.name的值不是局部變量name的值,而是全局變量name的值?

  在這裏首先必需要說的是,this的指向在函數定義的時候是肯定不了的,只有函數執行的時候才能肯定this到底指向誰,實際上this的最終指向的是那個調用它的對象。this是JavaScript的關鍵字,而不是變量,每一個函數調用都包含一個this的值,若是閉包在外部函數裏是沒法訪問到閉包裏面的this值的。由於這個this和當初定義函數時的this不是同一個,即使是同一個this,this的值是隨着調用棧的變化而變化的,而閉包裏的邏輯所取到的this的值也不是肯定的。因爲匿名函數的執行具備全局性,所以其this一般指向window。固然,咱們仍是有辦法來解決這種問題的,就是將this轉存爲一個變量就能夠避免this的不肯定性帶來的歧義。以下:

var name = "window";
var obj = {
    name : "object",
    getName : function(){
        var that = this;
        return function(){
            return that.name;
        }
    }
};
alert(obj.getName()());   // object

 

5、小試牛刀

看看點擊不一樣li標籤時,alert的值會是多少?

HTML:
<
ul> <li>0</li> <li>1</li> <li>2</li> <li>3</li> </ul>
JS:
window.onload = function(){ var aLi = document.getElementsByTagName('li'); for (var i=0;i<aLi.length;i++){ aLi[i].onclick = function(){ alert(i); }; }

  這是一道很經典的筆試題,也是不少初學者常常犯錯並且找不到緣由的一段代碼。想要實現的效果是點擊不一樣的<li>標籤,alert出其對應的索引值,可是實際上代碼運行以後,咱們會發現無論點擊哪個<li>標籤,alert出的i都爲4。爲何呢?由於在執行for循環以後,i的值已經變成了4,等到點擊<li>標籤時,alert的i值是4。下面就用閉包來解決這個問題。

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

你作對了嗎?

相關文章
相關標籤/搜索