原生JS執行環境與作用域深入理解
首先,我們要知道執行環境和作用域是兩個完全不同的概念。
函式的每次呼叫都有與之緊密相關的作用域和執行環境。從根本上來說,作用域是基於函式的,而執行環境是基於物件的(例如:全域性執行環境即window物件)。
換句話說,作用域涉及到所被呼叫函式中的變數訪問,並且不同的呼叫場景是不一樣的。執行環境始終是this關鍵字的值,它是擁有當前所執行程式碼的物件的引用。每個執行環境都有一個與之關聯的變數物件,環境中定義的所有變數和函式都儲存在這個物件中。雖然我們編寫的程式碼無法訪問這個物件,但解析器在處理資料時會在後臺使用它。
執行環境(也稱執行上下文–execution context)
當JavaScript直譯器初始化執行程式碼時,它首先預設進入全域性執行環境,從此刻開始,函式的每次呼叫都會建立一個新的執行環境
每個函式都有自己的執行環境。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中(execution stack)。在函式執行完後,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript程式中的執行流正是由這個便利的機制控制著。
執行環境可以分為建立和執行兩個階段。在建立階段,解析器首先會建立一個變數物件(variable object,也稱為活動物件 activation object),它由定義在執行環境中的變數、函式宣告、和引數組成。在這個階段,作用域鏈會被初始化,this的值也會被最終確定。在執行階段,程式碼被解釋執行。
Demo:
<script type="text/javascript">
function Fn1(){
function Fn2(){
alert(document.body.tagName);//BODY
//other code...
}
Fn2();
}
Fn1();
//code here
</script>
小結
當javascript程式碼被瀏覽器載入後,預設最先進入的是一個全域性執行環境。當在全域性執行環境中呼叫執行一個函式時,程式流就進入該被呼叫函式內,此時JS引擎就會為該函式建立一個新的執行環境,並且將其壓入到執行環境堆疊的頂部。瀏覽器總是執行當前在堆疊頂部的執行環境,一旦執行完畢,該執行環境就會從堆疊頂部被彈出,然後,進入其下的執行環境執行程式碼。這樣,堆疊中的執行環境就會被依次執行並且彈出堆疊,直到回到全域性執行環境。
此外還要注意一下幾點:
- 單執行緒
- 同步執行
- 唯一的全域性執行環境
- 區域性執行環境的個數沒有限制
- 每次某個函式被呼叫,就會有個新的區域性執行環境為其建立,即使是多次呼叫的自身函式(即一個函式被呼叫多次,也會建立多個不同的區域性執行環境)。
作用域
當代碼在一個環境中執行時,會建立變數物件的一個作用域鏈(scope chain)。作用域鏈的用途是保證對執行環境有權訪問的所有變數和函式的有序訪問。
作用域鏈包含了執行環境棧中的每個執行環境對應的變數物件。通過作用域鏈,可以決定變數的訪問和識別符號的解析。
注意:全域性執行環境的變數物件始終都是作用域鏈的最後一個物件。
在訪問變數時,就必須存在一個可見性的問題(內層環境可以訪問外層中的變數和函式,而外層環境不能訪問內層的變數和函式)。更深入的說,當訪問一個變數或呼叫一個函式時,JavaScript引擎將不同執行環境中的變數物件按照規則構建一個連結串列,在訪問一個變數時,先在連結串列的第一個變數物件上查詢,如果沒有找到則繼續在第二個變數物件上查詢,直到搜尋到全域性執行環境的變數物件即window物件。這也就形成了Scope Chain的概念。
特別說明:圖片來自於笨蛋的座右銘部落格
作用域鏈圖,清楚的表達了執行環境與作用域的關係(一一對應的關係),作用域與作用域之間的關係(連結串列結構,由上至下的關係)。
Demo:
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 這裡可以訪問color, anotherColor, 和 tempColor
}
// 這裡可以訪問color 和 anotherColor,但是不能訪問 tempColor
swapColors();
}
changeColor();
// 這裡只能訪問color
console.log("Color is now " + color);
上述程式碼一共包括三個執行環境:全域性執行環境、changeColor()的區域性執行環境、swapColors()的區域性執行環境。
- 全域性環境有一個變數color和一個函式changecolor();
- changecolor()函式的區域性環境中具有一個anothercolor屬性和一個swapcolors函式,當然,changecolor函式中可以訪問自身以及它外圍(即全域性環境)中的變數;
- swapcolor()函式的區域性環境中具有一個變數tempcolor。在該函式內部可以訪問上面的兩個環境(changecolor和window)中的所有變數,因為那兩個環境都是它的父執行環境。
上述程式碼的作用域鏈如下圖所示:
從上圖發現。內部環境可以通過作用域鏈訪問所有的外部環境,但是外部環境不能訪問內部環境中的任何變數和函式。
識別符號解析(變數名或函式名搜尋)是沿著作用域鏈一級一級地搜尋識別符號的過程。搜尋過程始終從作用域鏈的前端開始,然後逐級地向後(全域性執行環境)回溯,直到找到識別符號為止。
執行環境與作用域的區別與聯絡
執行環境為全域性執行環境和區域性執行環境,區域性執行環境是函式執行過程中建立的。
作用域鏈是基於執行環境的變數物件的,由所有執行環境的變數物件(對於函式而言是活動物件,因為在函式執行環境中,變數物件是不能直接訪問的,此時由活動物件(activation object,縮寫為AO)扮演VO(變數物件)的角色。)共同組成。
當代碼在一個環境中執行時,會建立變數物件的一個作用域鏈。作用域鏈的用途:是保證對執行環境有權訪問的所有變數和函式的有序訪問。作用域鏈的前端,始終都是當前執行的程式碼所在環境的變數物件。
小練習
<script type="text/javascript">
(function(){
a= 5;
console.log(window.a);//undefined
var a = 1;//這裡會發生變數宣告提升
console.log(a);//1
})();
</script>
window.a之所以是undefined,是因為var a = 1;發生了變數宣告提升。相當於如下程式碼:
<script type="text/javascript">
(function(){
var a;//a是區域性變數
a = 5;//這裡區域性環境中有a,就不會找全域性中的
console.log(window.a);//undefined
a = 1;//這裡會發生變數宣告提升
console.log(a);//1
})();
</script>