javascript代碼在執行時,會進入一個執行上下文中,執行上下文可以理解為當前代碼的運行環境。
javascript中運行環境主要包括以下三種情況
> 1 全局環境:代碼運行起來首先會進入全局環境
> 2 函數環境:當函數被調用執行時,會進入當前函數中執行代碼
> 3 eval函數環境:不建議使用,這裡不做介紹。
所以在一個javascript程序中,必定會出現多種不同的執行上下文。javascript是一個單線程語言,這意味著在瀏覽器中同時只能做一件事情。當javascript解釋器初始執行代碼,它首先默認進入全局上下文。每次調用一個函數將會創建一個新的執行上下文。每次新創建的一個執行上下文會被添加到作用域鏈的頂部,有時也稱為執行或調用棧。瀏覽器總是運行位於作用域鏈頂部的當前執行上下文。一旦完成,當前執行上下文將從棧頂被移除並且將控制權歸還給之前的執行上下文。
不同執行上下文之間的變量命名衝突通過攀爬作用域鏈解決,從局部直到全局。這意味著具有相同名稱的局部變量在作用域鏈中有更高的優先級。簡單的說,每次你試圖訪問函數執行上下文中的變量時,查找進程總是從自己的變量對象開始。如果在自己的變量對象中沒發現要查找的變量,繼續搜索作用域鏈。它將攀爬作用域鏈檢查每一個執行上下文的變量對象,尋找和變量名稱匹配的值。
2>執行上下文的建立過程
我們現在已經知道,每當調用一個函數時,一個新的執行上下文就會被創建出來。然而,在javascript引擎內部,這個上下文的創建過程具體分為兩個階段:
>建立階段(發生在當調用一個函數時,但是在執行函數體內的具體代碼以前)
- - - 建立變量,函數,arguments對象,參數
- - - 建立作用域鏈
- - - 確定this的值
代碼執行階段:
- - - 變量賦值,函數引用,執行其它代碼
3>建立階段以及代碼執行階段的詳細分析
確 切地說,執行上下文對象(上述的executionContextObj)是在函數被調用時,但是在函數體被真正執行以前所創建的。函數被調用時,就是我 上述所描述的兩個階段中的第一個階段 - 建立階段。這個時刻,引擎會檢查函數中的參數,聲明的變量以及內部函數,然後基於這些信息建立執行上下文對象 (executionContextObj)。在這個階段,variableObject對象,作用域鏈,以及this所指向的對象都會被確定。
上述第一個階段的具體過程如下:
找到當前上下文中的調用函數的代碼
在執行被調用的函數體中的代碼以前,開始創建執行上下文
進入第一個階段-建立階段:
建立variableObject對象:
建立arguments對象,檢查當前上下文中的參數,建立該對象下的屬性以及屬性值
檢查當前上下文中的函數聲明:
每找到一個函數聲明,就在variableObject下面用函數名建立一個屬性,屬性值就是指向該函數在內存中的地址的一個引用
如果上述函數名已經存在於variableObject下,那麼對應的屬性值會被新的引用所覆蓋。
檢查當前上下文中的變量聲明:
每找到一個變量的聲明,就在variableObject下,用變量名建立一個屬性,屬性值為undefined。
如果該變量名已經存在於variableObject屬性中,直接跳過(防止指向函數的屬性的值被變量屬性覆蓋為undefined),原屬性值不會被修改。
初始化作用域鏈
確定上下文中this的指向對象
代碼執行階段:
執行函數體中的代碼,一行一行地運行代碼,給variableObject中的變量屬性賦值。
「實例1」
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColor() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColor();
}
changeColor();
我們用ECStack來表示處理執行上下文的堆棧第一步全局上下文入棧,並一直存在於棧底,如圖所示:
第二步,全局上下文入棧之後,從可執行代碼開始執行,直達遇到changeColor(),這句代碼激活了函數changeColor,從而創建changeColor自己的執行上下文,因此此時是changeColor ECStack的上下文入棧。
第三步,changeColor EC 的上下文入棧之後,開始執行其中的可執行代碼,並在遇到swapColor()這句代碼之後激活swapColor()執行上下文,因此第三步就是swapColor EC的上下文入棧
第四步,在swapColor的可執行代碼中,沒有其他能生成執行上下文的情況,因此這段代碼順利執行完畢,swapColor的執行上下文入棧中彈出,如圖所示
第五步,swapColor的執行上下文彈出之後,繼續執行changeColor的可執行代碼,遇到其他執行上下文,順利執行完畢後彈出,這樣ECStack就剩下全局執行上下文了,如圖所示
最後,,全局執行上下文在瀏覽器關閉後出棧。需要注意的是,函數執行上下文遇到 return 能直接終止可執行代碼的執行,因此會直接將當前上下文彈出棧。
「實例2」
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result();
這是一個閉包的例子,整個例子有一定的迷惑性但是我們只需要根據「函數執行才會創建執行上下文」這一個原則來理解,那麼這段代碼執行時的函數調用棧順序就會比較清晰了。第一步,仍然是全局上下文先入棧,如圖所示
第二步,就是全局代碼在執行過程中,遇到了f1()函數,執行var result = f1();,因此f1會創建對應的執行上下文併入棧,
第三步,在f1的可執行代碼中,雖然聲明了一個函數f2,但是並沒有執行任何函數,因此也就不會產生別的執行上下文,代碼執行結束後,f1自然會出棧,如圖所示
第四步,f1出棧之後,繼續執行全局上下文的代碼,這個時候遇到了result(),result()函數會創建一個新的執行上下文,因此這個時候result的上下文入棧,如圖
第五步,這個result()其實就是在f1中聲明的函數f2,因此這個時候就會執行f2的代碼,由於f2中沒有產生新的執行上下文,因此執行完畢後直接出棧