要想完全明白JavaScript的閉包,要先明白js中的一些基礎原理,然后我再給出一些例子來講解閉包。
在執行JavaScript時會創建一個執行環境(excution context),執行環境定義了變量或函數可以訪問的其他數據。每個執行環境都有一個與之關聯的變量對象(variable object 有些地方叫域對象(Scope object)),在執行環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在后台使用它。
全局執行環境是最外層的一個執行環境。根據js實現的宿主環境的不同,環境對象不一樣。瀏覽器中,全局執行環境是window,node.js的全局變量是global,所有的全局變量和方法都保存在全局對象中。
每個函數都有自己的執行環境。當調用進入一個函數時,函數的執行環境就會被創建。代碼在執行環境中運行時,他創建用於保存變量對象的作用域鏈(scope chain)。他的作用是保存一個執行環境所有可以訪問的變量或函數的有序集合。作用域的最前面是當前執行的代碼所在執行環境的變量對象。如果當前的執行環境是一個函數,就將函數的活動對象作為變量對象,剛開始時只有一個變量arguments。作用域鏈中的下一個變量對象是包含當前環境變量的外部環境也就是他的調用者,再下一個是更外層的,至到全局執行環境。
所以在一個執行中的方法內訪問一個不存在於這個執行環境中的變量時不會報錯,解析器會從作用域鏈的頂端的變量對象開始找,如果找不到就找下一個執行環境的變量對象,一直到全局環境變量。如果有則停止查找。如果找到全局變量對象還是沒有發現,就會報錯。
簡單說就是,一個函數體內就是一個執行環境,當一個函數在執行時,會創建一個作用鏈,這個鏈中有自己的變量對象,同時也有外層的變量對象。
示例1:全局執行環境
var value1 = 1; var value2 = 2;
直接運行上面的代碼,也就是說我們在一個全局執行環境中定義了兩個變量,所以他倆會被保存在全局對象中這里用global保存。如下圖所示
示例2.全局環境中的方法
var value1 = 11; var value2 = 22; function log() { var logValue = "writing... "; console.log(logValue, "value1 :", value1, " value2 ", value2); } log();
在log函數中,他的作用域鏈包含兩個對象:一個是自己的的變量對象(包含arguments對象)和全局環境變量對象,所以在函數內訪問value1和value2時就可以沿着作用域鏈找找他倆。
示例3:閉包(嵌套函數 )
var value1 = 11; var value2 = 22; function log() { var logValue = "writing... "; function nested() { console.log(logValue, "value1 :", value1, " value2 ", value2); } return nested; } var fun1 = log(); fun1();
當你在一個函數內又創建了函數,那么就會創建閉包。當函數開始執行時閉包就會在堆上分配堆棧幀,而且在函數返回時不會被釋放掉。在上面的代碼中有三個執行環境一個是全局執行環境,一個是log()的局部執行環境,還有一個是nested()的執行環境。nested()可以訪問log和global的變量.log()可以訪問自己的global的變量:
nested()函數從log()方法中被返回,他的作用域鏈被初始化為log()中定義的所有活動對象,和全局變量對象,這樣nested()函數就可以訪問所有的變量了。更重要的是log()執行完畢后,他的變量對象不會被銷毀,因為nested()函數仍然在引用這個變量對象。也可以說,log()函數執行完后,log()的作用域鏈被銷毀,但變量對象仍然保留在內存中,直到nested()銷毀后,引用的log()的變量對象才會被銷毀。
有c++或c經驗的程序員,可能會認為返回的是一個方法的指針,nested和fun1變量是兩個指向這個方法的指針,其實不然,c++言語指向方法的指針和JavaScript中對一個方法的引用有很大的不同,JavaScript中你可以認為一個方法的引用變量有一個指向方法的指針,同時也有一個隱藏的指針指向閉包。我就不再舉其他的例子了,能簡單明了的讓大家理解閉包的原理就夠了。