標籤:記憶體 ima ret height 開始 ext console 代碼 style
要想完全明白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中你可以認為一個方法的引用變數有一個指向方法的指標,同時也有一個隱藏的指標指向閉包。我就不再舉其他的例子了,能簡單明了的讓大家理解閉包的原理就夠了。
詳解JavaScript閉包