javascript中的閉包

  1. 簡要介紹

  閉包可謂是js中的一大特點了,即便你對閉包沒概念,你可能已經在不知不覺中使用到了閉包。閉包是什麼,閉包就是一個函數能夠訪問到另外一個函數的變量。這就是閉包,解釋起來就這麼一句話,不明白?咱們來看一個簡單的例子:javascript

function getName(){
    var name='wenzi';
    setTimeout(function(){
        console.log(name);
    }, 500);
}
getName();

  這就其實已是閉包了,setTimeout中的function是一個匿名函數,這個匿名函數裏的name是geName()做用域中的變量,匿名函數裏只有一個輸出語句:console.log();html

  還有一個很經典的例子也能夠幫助咱們理解什麼是閉包:前端

function create(){
    var i=0;
    // 返回一個函數,暫且稱之爲函數A
    return function(){
        i++;
        console.log(i);
    }
}
var c = create(); // c是一個函數
c(); // 函數執行
c(); // 再次執行
c(); // 第三次執行

  在上面的例子中,create()返回的是一個函數,咱們暫且稱之爲函數A吧。在函數A中,有兩條語句,一條是變量i自增(i++),一條是輸出語句(console.log)。第一次執行執行c()時會產生什麼樣的結果?嗯,輸出自增後的變量i,也就是輸出1;那麼第二次執行c()呢,對,會輸出2;第三次執行c()時會輸出3,依次累加。這個create()函數依然知足了咱們在剛開始時的定義,函數A使用到了另外一個函數create()中的變量i。java

  但是爲何會產生這樣的輸出呢,爲何i就能一直自增呢,create函數已經執行完並返回結果了呀,但是爲何還能接着使用i呢,並且i還能自增。這裏就涉及到了三個比較重要的概念,講解完這三個概念,咱們對閉包就能夠有一個比較好的理解了。程序員

  2. 三個重要概念

  本節的大部分解釋來自於《JavaScript高級程序(第3版)》web

  2.1 執行環境與變量對象

  執行環境是JavaScript中一個重要的概念,它決定了變量或函數是否有權訪問其餘的數據,決定了它們各自的行爲。每一個執行環境都有一個與之對應的變量對象,執行環境中定義的全部變量和函數都保存在這個對象中。雖然咱們的代碼沒法訪問這個對象,可是解析器在處理數據時會在後臺使用它。瀏覽器

咱們用一個比較簡單的比喻來形容這兩個概念。執行環境就是一我的,變量對象就是這我的的身份證號,每一個人都有其對應的身份證號。他這我的決定了他身上的不少屬性和方法(動做),並且這我的的屬性和方法都在他的身份證號上,當這我的消亡的時候,身份證號也就隨之就註銷了。安全

  全局執行環境是最外層的一個執行環境。在web瀏覽器中,全局執行環境被認爲是window對象,由於全部的全局變量和全局函數都是做爲window對象的屬性和方法建立的。某個執行環境中的全部代碼執行完畢後,該環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬(全局執行環境直到應用程序退出——例如關閉網頁或者瀏覽器——時纔會被銷燬),被垃圾回收機制回收。閉包

  每一個函數都有本身的執行環境。當執行流進入一個函數時,函數的環境就會被推入到一個環境棧中。而在函數執行後,棧將其環境彈出,把控制權返回給以前的執行環境。函數

  2.2 做用域鏈

  做用域鏈是當代碼在一個環境中執行時建立的,做用域鏈的用途就是要保證執行環境中能有效有序的訪問全部變量和函數。做用域鏈的最前端始終都是當前執行的代碼所在環境的變量對象,下一個變量對象是來自其父親環境,再下一個變量對象是其父親的父親環境,直到全局執行環境。

  標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直到找到標識符爲止(若是找不到標識符,一般會致使錯誤發生)。其實,通俗的理解就是:在本做用域內找不到變量或者函數,則在其父親的做用域內尋找,再找不到則到父親的父親做用域內尋找,直到在全局的做用域內尋找!

  2.3 垃圾回收機制

  在js中有兩種垃圾收集的方式:標記清除和引用計數。

  標記清除:垃圾收集器在運行時會給存儲在內存中的全部變量都加上標記(具體的標記方式暫時就不清楚了),待變量已不被使用或者引用,去掉該標記或添加另外一種標記。最後,垃圾收集器完成內存清除工做,銷燬那些已沒法訪問到的這些變量並回收他們所佔用的空間。

  引用計數:通常來講,引用計數的含義是跟蹤記錄每一個值被引用的次數。當聲明一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數即是1,若是同一個值又被賦給另外一個變量,則該值的引用次數加1,相反,若是包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減1。當這個值的引用次數爲0時,說明沒有辦法訪問到它了,於是能夠將其佔用的內存空間回收。

  除了一些極老版本的IE,目前市面上的JS引擎基本採用標記清除來除了垃圾回收。可是須要注意的是IE中的DOM因爲機制問題,是採用了引用計數的方式,因此會有循環引用的問題,形成內存泄露。

var ele = document.getElementById(「element」);
var obj = new Object();
ele.obj = obj; // DOM元素ele的obj引用obj變量
obj.ele = ele; // obj變量的ele引用了DOM元素ele

   這樣就形成了循環引用的問題,致使垃圾回收機制回收不了ele和obj。不過,能夠在不使用ele和obj時,對這兩個變量進行 null 賦值,而後垃圾回收機制就會回收它們了。

  3. 理解閉包

  在第2部分講解了三個重要的概念,這三個概念有助於咱們更好的理解閉包。

  咱們再次拿出上面的這個例子:

function create(){
    var i=0;
    // 返回一個函數,暫且稱之爲函數A
    return function(){
        i++;
        console.log(i);
    }
}
var c = create(); // c是一個函數,即函數A
c(); // 函數執行
c(); // 再次執行
c(); // 第三次執行

   從上面的「每一個函數都有本身的執行環境」能夠知道:create()函數是一個執行環境,函數A也是一個執行環境,且函數A的執行環境在create()的裏面。這樣就造成了一個做用域鏈:window->create->A。當執行c()時,函數A就會首先在當前執行環境中尋找變量i,但是沒有找到,那麼只能順着做用域鏈向後找;OK,在create()的執行環境中找到了,那麼就可使用了變量i了。

  但是咱們還有一個疑問,按照上面的說法,函數create()執行完畢後,這個函數與裏面的變量和方法應該被銷燬了呀,但是爲何函數c()屢次執行時依然可以輸出變量i呢。這就是閉包的獨特之處。

  函數create()執行完畢後,雖然它的做用域鏈會被銷燬,即再也不存在window->create這個鏈式關係,可是函數A()[c()]的做用域鏈還依然引用着create()的變量對象,還存在着window->create->A的鏈式關係,致使垃圾回收機制不能回收create()的變量對象,create()的變量對象仍然停留在內存中,直到函數A()[c()]被銷燬後,create()的變量對象纔會被銷燬。

  所以,雖然create()已經執行完畢了,可是create()的變量對象並無被回收,還停留在內存中,依然可使用。

  從上面的講解中咱們能夠看到,閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。當頁面中存在過多的閉包,或者閉包的嵌套不少很深時,會致使內存佔用過多。所以,在這裏建議:慎用閉包。

  4. 閉包與變量

  有不少新手爲DOM元素綁定事件時,一般會這麼寫:

function bindClick(){
    var li = document.getElementsByTagName('li'); // 假設一共有5個li標籤
    for(var i=0; i<li.length; i++){
        li[i].onClick = function(){
            console.log('click the '+i+' li tag');
        }
    }
}

   他的本意是想爲每一個li標籤綁定一個單獨的事件,點擊第幾個li標籤,就能輸出幾。但是,最後的結果倒是,點擊哪一個li標籤輸出的都是5,這是爲何呢?

  其實這位程序員寫的bindClick()已經構成了一個閉包,下面的這個函數有他的做用域,而變量i本不屬於這個函數的做用域,而是屬於bindClick()中的:

// 匿名函數
function(){
    console.log('click the '+i+' li tag');
}

   所以,這就構成了一個含有閉包的做用域鏈:window->bindClick->匿名函數。但是這跟輸出的i有關係麼?有。做用域鏈中的每一個變量對象保存的是對變量和方法的引用,而不是保存這個變量的某一個值。當執行到匿名函數時,bindClick()其實已經執行完畢了,變量i的值就是5,此時每一個匿名函數都引用着同一個變量i。

  不過咱們稍微修改一下,以知足咱們的預期:

function bindClick(){
    var li = document.getElementsByTagName('li');
    for(var i=0; i<li.length; i++){
        li[i].onClick = (function(j){
            console.log('click the '+j+' li tag');
        })(i);
    }
}

   在這裏,咱們使用當即執行的匿名函數來保證傳入的值就是當前正在操做的變量i,而不是循環完成後的值。

   5. 閉包的應用場景

  (1)在內存中維持一個變量。好比前面講的小例子,因爲閉包,函數create()中的變量i會一直存在於內存中,所以每次執行c(),都會給變量i加1.

  (2)保護函數內的變量安全。仍是那個小例子,函數create()中的變量c只有內部的函數才能訪問,而沒法經過其餘途徑訪問到,所以保護了變量c的安全。

  (3)實現面向對象中的對象。javascript並無提供類這樣的機制,可是咱們能夠經過閉包來模擬出類的機制,不一樣的對象實例擁有獨立的成員和狀態。

  這裏咱們看一個例子:

function Student(){
    var name = 'wenzi';

    return {
        setName : function(na){
            name = na;
        },

        getName : function(){
            return name;
        }
    }
}
var stu = new Student();
console.log(stu.name); // undefined
console.log(stu.getName()); // wenzi

   這就是一個用閉包實現的簡單的類,裏面的name屬性是私有的,外部沒法進行訪問,只能經過setName和getName進行訪問。

  固然,閉包還存在另一種形式,剛纔咱們已經使用到了:

;(function(w){
    console.log(w);
})(window)

   之前也寫過一次的閉包的文章,兩個能夠連着一塊兒看:http://www.cnblogs.com/xumengxuan/p/3753713.html 

 

  本文地址:http://www.xiabingbao.com/javascript/2015/05/23/javascript-closure/

相關文章
相關標籤/搜索