JavaScript閉包與變量的經典問題

許多人第一次接觸閉包大概都是從高程裏這段代碼開始的:javascript

function createFunctions() {
    var result = new Array();

    for(var i=0; i<10; i++) {
        result[i] = function() {
            return i;
        }
    }
    return result;
}
var foo = createFunction();

 或者是用for循環在給網頁中一連串元素綁定例如onclick事件時。java

全部的教材在講到這一點時都會給出這樣的解釋: 由於每一個函數都保存着createFunction中的活動對象,因此它們引用的都是同一個變量 i 。而循環結束後 i 的值爲10,因此每一個函數的輸出都是10.編程

解釋很是簡潔與正確。數組

然而仍是會有一部分人看了這個解釋後只知其一;不知其二,好比我。閉包

我第一次看到這個解釋後有了這麼一連串疑問: 雖然知道 i 最終是 10,可是在每次賦值過程當中 i 並非 10 啊,爲何非要取最後一個值呢?i 並非引用數據類型,爲何能夠說「它們引用的都是同一個變量 i ?編程語言

若是你和我同樣有這個疑問,其實對這個問題而言咱們不理解的地方並非閉包,可是這個問題被打上了一個嚴重的」閉包「標籤,致使很長一段時間裏我都覺得本身不瞭解閉包。函數

實際上,我不理解的並非閉包這個概念,而是更爲基礎的,函數調用的時機。spa

 

咱們把代碼中賦值的哪一段改一下:code

result[i] = function() {
    return j;
}

 把 i 改爲 j, 一個並無定義的變量。對象

若是咱們僅僅把改完以後的代碼貼到console裏運行,它是不會報錯的。由於雖然createFunctions被調用了,卻並未調用賦給result的函數。

只有繼續使用語句調用result中的某個元素:

result[0](1);

 這樣纔會拋出 undefined 錯誤。

這說明了一個問題:僅僅聲明某一個函數,引擎並不會對函數內部的任何變量進行查找或賦值操做。只會對函數內部的語法錯誤進行檢查(若是往內部函數加上非法語句,那麼不用調用也會報錯)。

 

因此開頭問題裏的循環語句:

for(var i=0; i<10; i++) 
    result[i] = function() 
        return i;

 我本來覺得它是這樣的:

 result[0] = function() { return 0; };
 result[1] = function() { return 1; };
 result[2] = function() { return 2; };

 實際上它是這樣的:

 result[0] = function() { return i; };
 result[1] = function() { return i; };
 result[2] = function() { return i; };

 數組裏的 i 和 函數裏的 i 並非一回事, 外面的是常量, 裏面的是變量。

而當咱們調用result[0]函數時, 這個函數執行到 return 語句,發現並無 i 這個變量,因而順着做用鏈去找,在createFunctions裏找到了已經變成10的 i ,因而輸出 10. 這個過程纔是閉包的尋找變量的過程。

 

根據這個思路尋找解決方案時思路就明確多了,只要在每次賦值過程當中,不讓 i 做爲變量,而是確確實實地利用當時 i 的值,方法就是將 i 做爲函數參數進行調用:

result[i] = (function(val) { return val; })(i);

 這樣一來在每一次賦值的過程當中,每個result[i]都與 i 的當前值產生了聯繫。

固然,這樣修改的問題在於,原題返回的是一個函數,這裏返回的倒是一個值。

因此還要把返回值改爲相應的函數:

1 result[i] = (function (val) {
2   return function () {
3     return val;
4   };
5 })(i);

 這樣至關於給目標函數套上了一層塊級做用域,而且在 i 每次循環時都將它的值賦給了這個塊級做用域中的一個臨時變量。這個臨時變量其實和 i 沒有太大區別,只不過 i 在它的做用域聲明時值爲 0 ,結束後變成了10.而對每一個臨時變量而言,開始是多少,結束仍是多少。

 

進一步談閉包

任何聲明在另外一個函數內部的函數均可以稱爲閉包。也就是說,閉包是一個函數。不過也有些地方會講閉包是內部函數以及其做用域鏈組成的一個總體。兩種說法其實一個意思,畢竟嚴格來講,函數的做用域也是函數的一部分。不過我更喜歡後面一種說法,由於它強調了閉包的重點:維持做用域。

閉包主要有兩個概念:能夠訪問外部函數,維持函數做用域。第一個概念並無什麼特別,大部分編程語言都有這個特性,內部函數能夠訪問其外部變量這種事情很常見。因此重點在於第二點。舉例以下:

var globalValue;

function out() {
    var value = 1;
    function inner() {
        return value;
    }
    globalValue = inner;
}

out();

globalValue() // return 1;

 咱們先不考慮閉包地看一下這個問題:首先聲明瞭一個全局變量,而後調用了out函數,調用函數的過程當中全局變量被賦值了一個函數。out函數調用結束以後,按照內存處理機制,它內部的全部變量應該都被釋放掉了,不過還好咱們把inner複製給了全局變量,因此還能夠在外部調用它。接下來咱們調用了全局變量,這時候由於out內部做用域已經被釋放了,因此應該找不到value的值,返回應該是undefined。

可是事實是,它的確返回了 1,即內部變量。本該已經消失了,只能存在於out函數內部的變量,走到了牆外。這就是閉包的強大之處。

相關文章
相關標籤/搜索