Javascript語言在設計之初,就將函數設計成一種包含可執行代碼邏輯的特殊對象。作為對象,函數可以像普通對象變量一樣擁有可以編程讀寫的屬性,也可以像普通變量一樣傳遞、被引用。但是問題也來了,當函數執行時,解釋器如何對代碼內部的標示符進行解析呢?JS是這樣做的,當函數對象被創建時,或者說函數被定義時,函數對象內部不僅包含了代碼邏輯,還定義了一個內部屬性[[Scope]]引用了一條作用域鏈(可以理解成為一個對象列表)。如果這個函數在全局環境下被定義,那這個作用域鏈里就只有全局作用域。
這樣說比較抽象,我們來直接分析下面的全局函數:
var a = function(x){ var b = 'bb'; return b; };
當這樣簡單一個函數進入瀏覽器,瀏覽器中的js解釋器開始解釋代碼,因為在解釋代碼的時候會有標示符解析,所以在這之前,js解釋器需要掃描全局,初始化全局作用域,我們編程定義的a變量也會在這個時候就被放到全局作用域里(定義提前),但是a變量仍然是undefined。當解釋器自上而下解釋到這段代碼時,它會首先創建一個匿名函數,並將全局作用域壓進函數對象的內部屬性所引用的作用域鏈(對象列表)里。然后再把它賦值給變量a。
如下圖所示:
那么當我們執行a函數時又會發生什么呢?
a();
首先,會創建一個內部對象,我們稱這個內部對象為該函數的“執行期上下文”,一個函數的執行期上下文定義了一個函數執行的環境。函數每次執行都會創建獨一無二的執行期上下文,當函數執行完畢,該執行期上下文就會被銷毀。每個執行期上下文對象里都有自己的作用域鏈(對象列表),用來解析標示符。當執行期上下文被創建時,它的作用域鏈會被初始化為當前執行的函數對象里的[[Scope]]屬性中所包含的對象。這些值按照它們原有的順序被復制到執行期上下文的作用鏈里。這個過程一旦完成,就如圖所示:
然后,解釋器創建一個稱為“活動對象(activation object)”的新的內部對象,這個活動對象包含當前這個執行函數里所有的局部變量(內部var 聲明的),命名參數(形參),參數合集(arguments)以及this。然后此對象會被推入執行期上下文里作用域鏈的前端。如下圖:
這時候大家是不是想到了javascript函數作用域,變量聲明提前的問題呢?這個問題的根源就在這里。從上到下執行函數內部指令之前,其實解釋器就已經提前獲取了變量聲明了^_^。(變量聲明提前還有很多細節,比如聲明提前但賦值不提前,函數的兩種定義方式表現不一致的問題,在此就不細說了)。當函數執行上下文被創建好之后,解釋器開始自上至下的解釋代碼了,這時每遇到一個標示符,都會遍歷這個執行上下文里的作用域鏈:首先在最頂層的活動對象里去找,如果沒找到,再找下一層,最終找到全局作用域,如果還沒找到一般會報語法錯誤,但是當對未定義的變量執行賦值運算時,解釋器會在全局作用域創建該變量並賦值。這一點很重要,編程時要小心,防止污染全局環境,或者造成內存泄露的問題。
以上是個簡單的函數,如果出現了函數嵌套又會怎么樣呢?上代碼:
var a = function(x){ var b = 'bb'; var inner = function(){ var c = 'cc'; }; return b; };
在a函數里又定義了個inner函數,a函數在執行時,inner函數被定義,也就是說inner函數對象被創建,碼字太辛苦直接上圖了:
從圖中很明顯看到,inner函數對象在創建時內部[[Scope]]屬性指向的作用域鏈初始化為a函數執行期上下文的作用域鏈。當執行inner函數時也是先創建執行期上下文。執行期上下文的創建過程和執行a函數時的一樣,這里就不重復了。
如果再嵌套幾層,就會這樣迭代下去。。。。。
好吧,先說到這里,本來還想說說閉包的(不過相信大家已經找到他們之間的聯系了吧........^_^),不過時間有限,下次再說吧。
本文參照了《高性能Javascript》《Javascript權威指南》。
如有錯誤,不用客氣直接指出,大家一起學習一起成長。。。