這是大蝦的第一篇博文,大蝦試圖用最直白的語言去描述出所理解的東西,大蝦是菜鳥,水平有限,有誤的地方希望路過的朋友們務必指正,謝謝大家了。
從讀書時代一路走來,大蝦在學習的時候逐漸喜歡上了去追尋根源,這個東西到底是為什么?他有什么用處?他解決了什么問題?他是怎么被想到的?從這些問題當中,我們能夠學到非常多,大蝦深有體會。我相信,即使是這些東西在發明之時,就算是創始人也未必思考的這么周全,很多情況下,它一定是先遇到了什么實際問題之后,再去思考解決方案。也就是說,每一個新知識新東西的提出,一定是為着解決某個問題而出現的,否則他的存在就沒有意義,而脫離了實際問題的學習也是沒有意義的。在學習這個東西的過程中,大蝦很看重的是,假如同樣的是遇到了這個問題,大蝦會怎么做去解決?別人又是怎么解決的?為什么別人的解決方案這么優秀?他是怎么想到的?我怎么就想不到呢?這種過程使大蝦受益匪淺。
話不多說,切入正題。本文主要介紹閉包。相信很多人只知道學閉包,直接去看他的原理,實現過程什么的,但是根據我的了解來看,初學者知道閉包的用途的並不多,為什么需要閉包?這些都不清楚,然而在大蝦看來,這個非常重要,因為它對應着實際問題的解決,理論脫離了實踐將變得毫無意義。
話說二十年前,祖師爺創立js的時候,那時候頁面並不復雜,大型的網頁也不多,頁面沒什么js的時候,人們寫頁面時全局變量是隨便定義的,直到某一天,隨着頁面js的增多,問題來了。拿一個模擬的alert來說,如果這么寫:
var temp=a; var abs=function(){}; var yourAlert=function(){};
那么在這種情況下,全局變量temp,abs,yourAlert就被污染了。也就是說,如果要實現另外一個功能,比如說按鈕btn,這個時候也需要寫自己的代碼,那么它的變量起名必須重新起名,必須避開上面的變量,否則就會把上面的內容給覆蓋掉。這些新功能一旦數量很多,那么你的起名就必須避開所有的已用過的變量名,你必須挨個檢查所有功能的變量名以保證他的不重復,這樣就給開發帶來很大的不方便。所以迫切的需要一種方法來避免它,來保護變量不被篡改污染。再者,在頁面中,經常遇到多次調用的情況,同樣以上面的alert為例,假如用戶觸發了10次alert,如果說每一次的觸發都要重新創建一個alert的話,那樣豈不是特別麻煩,特別消耗內存?這個時候同樣需要一種能夠反復調用的方法來優化代碼的執行性能。
閉包應運而生。
來看看閉包的實現過程,他到底是如何實現並且達到上述目的並解決實際問題的呢?深刻的理解整個本質的實現過程有助於我們的開發運用。
當一個函數被調用時,一個執行環境(也稱執行上下文)就會被創建(execution context),然而在js引擎內部,這個執行環境創建過程被分為了2個階段:
1、 建立階段(此時還並沒有執行具體的函數體的代碼)
建立變量對象(variable object):函數里面的arguments對象、函數參數、內部變量、函數聲明
1、 建立arguments對象,檢查當前環境下的參數,建立該對象下的屬性和屬性值
2、 檢查當前環境下的函數聲明:每找到一個函數聲明,就會在變量對象里用函數名建立一個屬性,屬性值就是指向函數地址的引用。如果該函數名已經存在,那么其對應的屬性值就會指向新的引用。
3、 檢查當前環境下的變量聲明:每找到一個變量聲明,就在變量對象下建立一個屬性,其值為undefined(此時還未賦值)。如果該變量名已經存在,會直接跳過(防止指向函數的屬性值被變量屬性覆蓋為undefined)
建立作用域鏈
當前變量對象被添加到執行環境的前端
確定this的值
2、 代碼執行階段
執行函數中的代碼,對變量賦值、函數引用、執行其他代碼等等…
具體的來看一個函數的代碼:
1 function f(x){ 2 var a=20; 3 var b=function(){ 4 5 }; 6 function d(){ 7 8 }; 9 }
f(100);
在調用f(100)的時候,執行環境的建立階段發生如下變化:
fExecutionContext{ // f函數執行環境
variableObject:{// f函數變量對象(對於函數來說,也稱為活動對象AO)
arguments:{
0:100;
length:1;
},
x:100,
d:pointer to function d()//指向d函數的引用,實際上保存的是地址,它的順序也在變量聲明之上
a:undefined,
b:undefined,
},
scopeChain:{....},//作用域鏈
this:{...}// this值
}
當上述建立階段結束,js引擎立馬進入執行階段,一行一行的運行函數代碼,給variableObject的屬性賦值,執行階段完成如下:
fExecutionContext{ // f函數執行環境 variableObject:{// f函數變量對象(對於函數來說,也稱為活動對象AO) arguments:{ 0:100; length:1; }, x:100, d:pointer to function d()//指向d函數的引用,實際上保存的是地址,它(函數聲明)的順序也在變量聲明之上 a:20, b:pointer to function b(), }, scopeChain:{....},//作用域鏈 this:{...}// this值 }
事實上,如果這個環境是函數,變量對象並不能夠被直接訪問到,此時函數的活動對象(AO)將代替變量對象的角色。我們可以看到,上述環境中,執行環境的作用域鏈也被創建完成。
那作用域鏈又是什么呢?事實上,在函數的創建時,會預先創建一個包含全局變量對象的作用域鏈。作用域鏈的本質是一個指向變量對象的指針列表!它只是引用而實際上並不包含變量對象!為了方便理解,可以把它想象成一根鏈條(實際上並不是真的存在這么一根鏈條),上面依次標記着順序0,1,2,3.......,每一個數字對應着一個變量對象。在訪問變量對象中的屬性時,只能按照標記的順序按0,1,2,3...的順序依次訪問,在這個鏈條的最前端,始終是當前執行的代碼所在的環境的變量對象(可以理解為順序標記為0),創建執行環境時,當前函數的活動對象將會被推入到作用域鏈的最前端!而下一個變量對象則來自其外部函數(順序標記為1),再下一個則是外部函數的外部函數,全局執行環境的變量對象始終是鏈條的最后位置,全局的變量對象始終是最后一個被訪問到。
在本例中,以函數f為例,其作用域鏈關系如下圖
在尋找變量名和函數名的時候,會首先在順序為0的位置的變量對象中尋找(也就是它自己的活動對象),如果找不到,就會向下一級變量對象尋找(函數的外部函數的變量對象,在作用域鏈中順序標記為1),一直搜索到最后一個對象——全局環境的變量對象為止。全局環境的變量對象始終存在於每一個作用域鏈中!當函數執行完畢后,函數的活動對象就會被銷毀,內存中僅保存全局變量對象。
然!而!且!慢!閉包的情況不一樣啊!
看下面這個超級簡單的例子。
function f(){ var a=20; return function d(){ a--; }; }
var sB=f(); sB();
上面的d函數是直接定義在函數f的內部中的,即是說函數f是函數d的外部函數,按照上面所述原則,在函數d的作用域鏈中,函數f的變量對象會被添加進函數d的作用域鏈中!此時函數d的作用域鏈一共有3個對象:函數d的活動對象會被標記為0,函數f的變量對象會被標記為1,全局變量對象標記為2,訪問時按照0,1,2的順序訪問,尋找變量時先在自己的活動對象里找,再去順序為2即函數f里面找,這樣就能訪問外部函數f中的所有變量。然而當函數f執行完畢時,它的執行環境的作用域鏈會被銷毀,但它的活動對象並沒有被銷毀,仍然保存在內存中,匿名函數d的作用域鏈仍然在引用着這個活動對象,直到匿名函數d被銷毀,函數f的活動對象才會被銷毀。這就是閉包的最大的不同之處!
我們來畫個流程圖,理解下閉包過程中所發生的變化。從函數的創建開始。
前方高能,多圖預警!請在wifi下點開,土豪請無視。
1、函數f的創建
2、函數f開始執行了
3、執行到 return語句
4、執行var sB=f();
5、調用函數d
6、執行完畢
這就是閉包的整個實現過程,閉包實現后,可以在全局反復調用內部函數d(),此時在即使全局定義相同的變量a,調用函數時,使用的值仍然是函數f的活動對象里面的值,外面的更改無法影響到局部變量a。這里只是做個簡單的介紹,閉包還有很多應用情況,實際情況也更加復雜,還有很長的路要走。
參考書籍:《JavaScript高級程序設計第3版》
參考內容: http://tieba.baidu.com/p/2348703848
http://blogread.cn/it/article/6178?f=sa