過很多談如何理解閉包的方法,但大多數文章,都是照抄或者解釋《Javascript高級程序設計(第三版)》對於閉包的講解,甚至例程都不約而同的引用高程三181頁‘閉包與變量’一節的那個“返回數組各個項,結果各個項的值都相同”的例程,還有些文章的講解過程上一步與下一步之間的跨度簡直就是一步登天,讓人反復看半天都無法理解。
閉包的理解需要很多概念做鋪墊,包括變量作用域鏈、執行環境、變量活動對象、引用式垃圾內存收集機制等,如果對本文涉及的這些概念不理解,可以去找本《Javascript高級程序設計(第三版)》好好看看並理解這些概念。
我自己也是在反復編寫一些例子並仔細認真的閱讀和理解高程三與閉包相關的概念和知識后,終於對閉包有了一定的理解,為了檢驗學習效果,特別寫篇文章以便檢驗自己是否真正理解和掌握。
在理解閉包之前,我們先來看些理解閉包容易忽略的小東西。
//例子1:
function foo(){ var a = 1; return a; }; console.log(foo()); //1
上面的這個例子很好理解,一個函數如果return一個值,那么這個函數就可以當作return的值直接使用,無論它return的是一個引用類型(函數、數組)還是基本數據類型。
//例子2:
function foo(){ function foos(){ var b = 2; return b; } }; console.log(foos()); //Uncaught ReferenceError: foos is not defined
這個例子說明,如果一個函數B被聲明在一個函數A的內部,那么在函數A外部,通過函數B的函數名直接調用函數B,是會出未定義錯誤的。你可以理解為:變量作用域規則中的局部作用域規則對於函數聲明同樣成立。
//例子3:
function foo(){ var a = 1; function foos(){ return a;//注意:返回的a是foo中定義的變量,而不是foos中定義的變量。 } console.log(foos()); }; foo();//1
例子3說明foos只能在foo內部執行,這符合我們對於“變量作用域規則中的局部作用域規則,對於函數聲明同樣成立”的理解。
//例子4:
function foo(){ var arr = new Array();
for(var i=0;i<10;i++){ arr[i] = function(){ return i; }; } return function(){ for(var k = 0;k<arr.length;k++){ console.log(arr[k]()); } }; }; console.log(foo()());//輸出10個10;
例子4中中執行foo()以后得到的是foo自己返回的一個匿名函數(如下所示):
return function(){ for(var k = 0;k<arr.length;k++){ console.log(arr[k]()); } };
那么,此時我們還需要執行一遍這個得到的匿名函數,才能得出最后的值(也就是:10個10),而執行這個得到的匿名函數(我們不妨叫它函數C)時,foo已經執行了一遍了,這個時候foo的變量i的值已經是10了,由於變量對象是通過引用賦值的,所以這個時候foo內的閉包函數(也就是引用了foo的變量i的匿名函數)再來引用foo的變量i,那么得到的值自然就是10了。這里的關鍵在與foo內的閉包函數執行的時機,如果閉包函數立即執行后再賦值給arr[i],那么就可以得到我們期望的值0到9了;
//例子5:
function foo(){ var arr = new Array(); for(var i=0;i<10;i++){ arr[i] = function(){ return i; }(); } return function(){ for(var k = 0;k<arr.length;k++){ console.log(arr[k]); } }; }; console.log(foo()());//0到9
關於例子4更詳細的解釋:
在例子4中,根據javascript的引用垃圾收集機制(這個機制的規則是:存在大於0個引用的變量不應該被銷毀),雖然此時函數foo已經執行完了,但是foo的這個變量i因為被匿名函數(我們不妨叫它函數B)引用,所以這個i還是沒有被垃圾收集程序銷毀,那么這個i此時存在哪里呢?肯定不是存在於函數foo的執行環境里面,因為執行環境創建和銷毀的規則是:當一個函數創建時,則創建這個函數的執行環境,一旦這個函數執行完畢則馬上銷毀這個執行環境。因此這個i只可能存在於foo內部的匿名函數(函數B)的執行環境中,這個函數B的執行環境中不但有自己的作用域和自己的變量對象,而且包含了其外部函數foo的作用域和foo的變量對象,變量i就存在於函數B包含的foo的作用域中和變量對象中。由於javascript不能直接操作內存空間,所以javascript只能以引用的方式訪問變量對象,當foo的變量i的值已經變成10了的時候,再去執行foo的閉包函數(函數B),那么此時對i的引用自然會得到10。
使用以上實驗得出的原理,我們還可以很容易的理解《Javascript高級程序設計(第三版)》181頁最下面的那個例子:
//《Javascript高級程序設計(第三版)》181頁最下面的那個例子: function createFunctions(){ var result = new Array(); for(var i=0;i<10;i++){ result[i] = function(num){ return function(){ return num; } }(i);//這里是將參數傳給該匿名函數,並且立即執行該匿名函數的寫法; } return result; } for(var i=0;i<10;i++){ console.log(createFunctions()[i]()); }//0,1,2,3,4,5,6,7,8,9
同時,我們可以很容易的改寫這個例子:
function createFunctions(){ var result = new Array(); for(var i=0;i<10;i++){ result[i] = function(){ return function(){ return i; }()//這里添加了立即執行 }();//這里是將參數傳給該匿名函數,並且立即執行該匿名函數的寫法; } return result; } for(var i=0;i<10;i++){ console.log(createFunctions()[i]);//這里去掉了一對括號 }//0,1,2,3,4,5,6,7,8,9
這個改寫的例子同時證明,最內層的匿名函數還是可以引用最外面全局函數的變量。
下面這張圖片中的例子,使用閉包就可以很好的理解了:

要是實在不理解,還可以試着這樣來驗證一下幫助理解:
function foo(){ var diss = "mem_count"; return function(){ return diss; } } var fo = foo(); var as = fo(); //undefined as //"mem_count" var as2 = fo(); //undefined as2 //"mem_count" fo() //"mem_count" fo() //"mem_count" fo() //"mem_count"
如果這樣還是沒辦法理解,那真是應該先去看看變量的作用域、垃圾回收機制,函數表達式等基礎知識了。
