許多人第一次接觸閉包大概都是從高程里這段代碼開始的:
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事件時。
所有的教材在講到這一點時都會給出這樣的解釋: 因為每個函數都保存着createFunction中的活動對象,所以它們引用的都是同一個變量 i 。而循環結束后 i 的值為10,所以每個函數的輸出都是10.
解釋非常簡潔與正確。
然而還是會有一部分人看了這個解釋后一知半解,比如我。
我第一次看到這個解釋后有了這么一連串疑問: 雖然知道 i 最終是 10,但是在每次賦值過程中 i 並不是 10 啊,為什么非要取最后一個值呢?i 並不是引用數據類型,為什么可以說“它們引用的都是同一個變量 i ?
如果你和我一樣有這個疑問,其實對這個問題而言我們不理解的地方並不是閉包,但是這個問題被打上了一個嚴重的”閉包“標簽,導致很長一段時間里我都以為自己不了解閉包。
實際上,我不理解的並不是閉包這個概念,而是更為基礎的,函數調用的時機。
我們把代碼中賦值的哪一段改一下:
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函數內部的變量,走到了牆外。這就是閉包的強大之處。