這是大蝦的第一篇博文,大蝦試圖用最直白的語言去描述出所理解的東西,大蝦是菜鳥,水平有限,有誤的地方但願路過的朋友們務必指正,謝謝你們了。javascript
從讀書時代一路走來,大蝦在學習的時候逐漸喜歡上了去追尋根源,這個東西究竟是爲何?他有什麼用處?他解決了什麼問題?他是怎麼被想到的?從這些問題當中,咱們可以學到很是多,大蝦深有體會。我相信,即便是這些東西在發明之時,就算是創始人也未必思考的這麼周全,不少狀況下,它必定是先遇到了什麼實際問題以後,再去思考解決方案。也就是說,每個新知識新東西的提出,必定是爲着解決某個問題而出現的,不然他的存在就沒有意義,而脫離了實際問題的學習也是沒有意義的。在學習這個東西的過程當中,大蝦很看重的是,假如一樣的是遇到了這個問題,大蝦會怎麼作去解決?別人又是怎麼解決的?爲何別人的解決方案這麼優秀?他是怎麼想到的?我怎麼就想不到呢?這種過程使大蝦受益不淺。前端
話很少說,切入正題。本文主要介紹閉包。相信不少人只知道學閉包,直接去看他的原理,實現過程什麼的,可是根據個人瞭解來看,初學者知道閉包的用途的並很少,爲何須要閉包?這些都不清楚,然而在大蝦看來,這個很是重要,由於它對應着實際問題的解決,理論脫離了實踐將變得毫無心義。java
話說二十年前,祖師爺創立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個階段:優化
一、 創建階段(此時還並無執行具體的函數體的代碼)this
創建變量對象(variable object):函數裏面的arguments對象、函數參數、內部變量、函數聲明spa
一、 創建arguments對象,檢查當前環境下的參數,創建該對象下的屬性和屬性值
二、 檢查當前環境下的函數聲明:每找到一個函數聲明,就會在變量對象裏用函數名創建一個屬性,屬性值就是指向函數地址的引用。若是該函數名已經存在,那麼其對應的屬性值就會指向新的引用。
三、 檢查當前環境下的變量聲明:每找到一個變量聲明,就在變量對象下創建一個屬性,其值爲undefined(此時還未賦值)。若是該變量名已經存在,會直接跳過(防止指向函數的屬性值被變量屬性覆蓋爲undefined)
創建做用域鏈
當前變量對象被添加到執行環境的前端
肯定this的值
二、 代碼執行階段
執行函數中的代碼,對變量賦值、函數引用、執行其餘代碼等等…
具體的來看一個函數的代碼:
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下點開,土豪請無視。
一、函數f的建立
二、函數f開始執行了
三、執行到 return語句
四、執行var sB=f();
五、調用函數d
六、執行完畢
這就是閉包的整個實現過程,閉包實現後,能夠在全局反覆調用內部函數d(),此時在即便全局定義相同的變量a,調用函數時,使用的值仍然是函數f的活動對象裏面的值,外面的更改沒法影響到局部變量a。這裏只是作個簡單的介紹,閉包還有不少應用狀況,實際狀況也更加複雜,還有很長的路要走。
參考書籍:《JavaScript高級程序設計第3版》
參考內容: http://tieba.baidu.com/p/2348703848
http://blogread.cn/it/article/6178?f=sa