1. 程式人生 > >js中的閉包之我理解

js中的閉包之我理解

閉包是一個比較抽象的概念,尤其是對js新手來說.書上的解釋實在是比較晦澀,對我來說也是一樣.

  但是他也是js能力提升中無法繞過的一環,幾乎每次面試必問的問題,因為在回答的時候.你的答案的深度,對術語的理解以及js內部直譯器的運作方式的描述,都是可以看出你js實際水平的.即使你沒答對,也能讓考官對你的水平有個評估.那麼我先來說說我對js中的閉包的理解.

  閉包是很多語言都具備的特性,在js中,閉包主要涉及到js的幾個其他的特性:作用域鏈,垃圾(記憶體)回收機制,函式巢狀,等等.

  在理解閉包以前.最好能先理解一下作用域鏈的含義,簡單來說,作用域鏈就是函式在定義的時候建立的,用於尋找使用到的變數的值的一個索引,而他內部的規則是,把函式自身的本地變數放在最前面,把自身的父級函式中的變數放在其次,把再高一級函式中的變數放在更後面,以此類推直至全域性物件為止.當函式中需要查詢一個變數的值的時候,js直譯器會去作用域鏈去查詢,從最前面的本地變數中先找,如果沒有找到對應的變數,則到下一級的鏈上找,一旦找到了變數,則不再繼續.如果找到最後也沒找到需要的變數,則直譯器返回undefined.

  瞭解了作用域鏈,我們再來看看js的記憶體回收機制,一般來說,一個函式在執行開始的時候,會給其中定義的變數劃分記憶體空間儲存,以備後面的語句所用,等到函式執行完畢返回了,這些變數就被認為是無用的了.對應的記憶體空間也就被回收了.下次再執行此函式的時候,所有的變數又回到最初的狀態,重新賦值使用.但是如果這個函式內部又嵌套了另一個函式,而這個函式是有可能在外部被呼叫到的.並且這個內部函式又使用了外部函式的某些變數的話.這種記憶體回收機制就會出現問題.如果在外部函式返回後,又直接呼叫了內部函式,那麼內部函式就無法讀取到他所需要的外部函式中變數的值了.所以js直譯器在遇到函式定義的時候,會自動把函式和他可能使用的變數(包括本地變數和父級和祖先級函式的變數(自由變數))一起儲存起來.也就是構建一個閉包,這些變數將不會被記憶體回收器所回收,只有當內部的函式不可能被呼叫以後(例如被刪除了,或者沒有了指標),才會銷燬這個閉包,而沒有任何一個閉包引用的變數才會被下一次記憶體回收啟動時所回收.

也就是說,有了閉包,巢狀的函式結構才可以運作,這也是符合我們的預期的.然後,閉包還有一些特性,卻往往讓程式設計師覺得很難理解.

看看下面一段程式碼.

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=function(){
            alert(i)
        }
    }
};
foo();
result[0](); // 3
result[1](); // 3
result[2](); // 3

這段程式碼中,程式設計師希望foo函式中的變數i被內部迴圈的函式使用,並且能分別獲得他們的索引,而實際上,只能獲得該變數最後保留的值,也就是說.閉包中所記錄的自由變數,只是對這個變數的一個引用,而非變數的值,當這個變數被改變了,閉包裡獲取到的變數值,也會被改變.

解決的方法之一,是讓內部函式在迴圈建立的時候立即執行,並且捕捉當前的索引值,然後記錄在自己的一個本地變數裡.然後利用返回函式的方法,重寫內部函式,讓下一次呼叫的時候,返回本地變數的值,改進後的程式碼:

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=(function(j){
            return function(){
                alert(j);
            };
        })(i);
    }
};
foo();
result[0](); // 0
result[1](); // 1
result[2](); // 2