JavaScript的執行過程(深入執行上下文、GO、AO、VO和VE等概念)
- 2022 年 1 月 10 日
- 筆記
- javascript, js相關, 前端
JavaScript的執行過程
前言
編寫一段JavaScript程式碼,它是如何執行的呢?簡單來說,JS引擎在執行JavaScript程式碼的過程中需要先解析再執行。那麼在解析階段JS引擎又會進行哪些操作,接下來就一起來了解一下JavaScript在執行過程中的詳細過程,包括執行上下文、GO、AO、VO和VE等概念的理解。
1.初始化全局對象
首先,JS引擎會在執行程式碼之前,也就是解析程式碼時,會在我們的堆記憶體創建一個全局對象:Global Object(簡稱GO),觀察以下程式碼,在全局中定義了幾個變數:
示例程式碼:
var name = 'curry'
var message = 'I am a coder'
var num = 30
JS引擎內部在解析以上程式碼時,會創建一個全局對象(偽程式碼如下):
- 所有的作用域(scope)都可以訪問該全局對象;
- 對象裡面會包含一些全局的方法和類,像Math、Date、String、Array、setTimeout等等;
- 其中有一個window屬性是指向該全局對象自身的;
- 該對象中會收集我們上面全局定義的變數,並設置成undefined;
- 全局對象是非常重要的,我們平時之所以能夠使用這些全局方法和類,都是在這個全局對象中獲取的;
var GlobalObject = {
Math: '類',
Date: '類',
String: '類',
setTimeout: '函數',
setInterval: '函數',
window: GlobalObject,
...
name: undefined,
message: undefined,
num: undefined
}
2.執行上下文棧(調用棧)
了解了什麼是全局對象後,下面就來聊聊程式碼具體執行的地方。JS引擎為了執行程式碼,引擎內部會有一個執行上下文棧(Execution Context Stack,簡稱ECS),它是用來執行程式碼的調用棧。
(1)ECS如何執行?先執行誰呢?
- 無疑是先執行我們的全局程式碼塊;
- 在執行前全局程式碼會構建一個全局執行上下文(Global Execution Context,簡稱GEC);
- 一開始GEC就會被放入到ECS中執行;
(2)那麼全局執行上下文(GEC)包含那些內容呢?
- 第一部分:執行程式碼前。
- 在轉成抽象語法樹之前,會將全局定義的變數、函數等加入到Global Object中,也就是上面初始化全局對象的過程;
- 但是並不會真正賦值(表現為undefined),所以這個過程也稱之為變數的作用域提升(hoisting);
- 第二部分:程式碼執行。
- 對變數進行賦值,或者執行其它函數等;
下面就通過一幅圖,來看看GEC被放入ECS後的表現形式:

3.調用棧調用GEC的過程
接下來,將全局程式碼複雜化一點,再來看看調用棧調用全局執行上下文(GEC)的過程。
實例程式碼:
var name = 'curry'
console.log(message)
var message = 'I am a coder'
function foo() {
var name = 'foo'
console.log(name)
}
var num1 = 30
var num2 = 20
var result = num1 + num2
foo()
調用棧調用過程:
-
1.初始化全局對象。
- 這裡需要注意的是函數存放的是地址,會指向函數對象,與普通變數有所不同;
- 從上往下解析JS程式碼,當解析到foo函數時,因為foo不是普通變數,並不會賦為undefined,JS引擎會在堆記憶體中開闢一塊空間存放foo函數,在全局對象中引用其地址;
- 這個開闢的函數存儲空間最主要存放了該函數的父級作用域和函數的執行體程式碼塊;

-
2.構建一個全局執行上下文(GEC),程式碼執行前將VO的記憶體地址指向GlobalObject(GO)。

-
3.將全局執行上下文(GEC)放入執行上下文棧(ECS)中。

-
4.從上往下開始執行全局程式碼,依次對GO對象中的全局變數進行賦值。
- 當執行
var name = 'curry'時,就從VO(對應的就是GO)中找到name屬性賦值為curry; - 接下來執行
console.log(message),就從VO中找到message,注意此時的message還為undefined,因為message真正賦值在下一行程式碼,所以就直接列印undefined(也就是我們經常說的變數作用域提升); - 後面就依次進行賦值,執行到
var result = num1 + num2,也是從VO中找到num1和num2兩個屬性的值進行相加,然後賦值給result,result最終就為50; - 最後執行到
foo(),也就是需要去執行foo函數了,這裡的操作是比較特殊的,涉及到函數執行上下文,下面來詳細了解;

- 當執行
4.函數執行上下文
在執行全局程式碼遇到函數如何執行呢?
- 在執行的過程中遇到函數,就會根據函數體創建一個函數執行上下文(Functional Execution Context,簡稱FEC),並且加入到執行上下文棧(ECS)中。
- 函數執行上下文(FEC)包含三部分內容:
- AO:在解析函數時,會創建一個Activation Objec(AO);
- 作用域鏈:由函數VO和父級VO組成,查找是一層層往外層查找;
- this指向:this綁定的值,在函數執行時確定;
- 其實全局執行上下文(GEC)也有自己的作用域鏈和this指向,只是它對應的作用域鏈就是自己本身,而this指向為window。
繼續來看上面的程式碼執行,當執行到foo()時:
- 先找到foo函數的存儲地址,然後解析foo函數,生成函數的AO;
- 根據AO生成函數執行上下文(FEC),並將其放入執行上下文棧(ECS)中;
- 開始執行foo函數內程式碼,依次找到AO中的屬性並賦值,當執行
console.log(name)時,就會去foo的VO(對應的就是foo函數的AO)中找到name屬性值並列印;

5.變數環境和記錄
上文中提到了很多次VO,那麼VO到底是什麼呢?下面從ECMA新舊版本規範中來談談VO。
在早期ECMA的版本規範中:每一個執行上下文會被關聯到一個變數環境(Variable Object,簡稱VO),在源程式碼中的變數和函數聲明會被作為屬性添加到VO中。對應函數來說,參數也會被添加到VO中。
- 也就是上面所創建的GO或者AO都會被關聯到變數環境(VO)上,可以通過VO查找到需要的屬性;
- 規定了VO為Object類型,上文所提到的GO和AO都是Object類型;
在最新ECMA的版本規範中:每一個執行上下文會關聯到一個變數環境(Variable Environment,簡稱VE),在執行程式碼中變數和函數的聲明會作為環境記錄(Environment Record)添加到變數環境中。對於函數來說,參數也會被作為環境記錄添加到變數環境中。
- 也就是相比於早期的版本規範,對於變數環境,已經去除了VO這個概念,提出了一個新的概念VE;
- 沒有規定VE必須為Object,不同的JS引擎可以使用不同的類型,作為一條環境記錄添加進去即可;
- 雖然新版本規範將變數環境改成了VE,但是JavaScript的執行過程還是不變的,只是關聯的變數環境不同,將VE看成VO即可;
6.全局程式碼執行過程(函數嵌套)
了解了上面相關的概念和調用流程之後,就來看一下存在函數嵌套調用的程式碼是如何執行的,以及執行過程中的一些細節,以下面程式碼為例:
var message = 'global'
function foo(m) {
var message = 'foo'
console.log(m)
function bar() {
console.log(message)
}
bar()
}
foo(30)
-
初始化全局對象(GO),執行全局程式碼前創建GEC,並將GO關聯到VO,然後將GEC加入ECS中:
- foo函數存儲空間中指定的父級作用域為全局對象;

-
開始執行全局程式碼,從上往下依次給全局屬性賦值:
- 給message屬性賦值為global;

-
執行到foo函數調用,準備執行foo函數前,創建foo函數的AO:
- bar函數存儲空間中指定父級作用域為foo函數的AO;

-
創建foo函數的FEC,並加入到ECS中,然後開始執行foo函數體內的程式碼:
- 根據foo函數調用的傳參,給形參m賦值為30,接著給message屬性賦值為foo;
- 所以,m列印結果為30;

-
執行到bar函數調用,準備執行bar函數前,創建bar函數的AO:
- bar函數中沒有定義屬性和聲明函數,以空對象表示;

-
創建bar函數的FEC,並加入到ECS中,然後開始執行bar函數體內的程式碼:
- 執行
console.log(message),會先去bar函數自己的VO中找message,沒有找到就往上層作用域的VO中找; - 這裡bar函數的父級作用域為foo函數,所以找到foo函數VO中的message為foo,列印結果為foo;

- 執行
-
全局中所有程式碼執行完成,bar函數執行上下文出棧,foo函數AO對象失去了引用,進行銷毀。
-
接著foo函數執行上下文出棧,foo函數AO對象失去了引用,進行銷毀,同樣,foo函數AO對象銷毀後,bar函數的存儲空間也失去引用,進行銷毀。
總結:
-
函數在執行前就已經確定了其父級作用域,與函數在哪執行沒有關係,以函數聲明的位置為主;
-
執行程式碼查找變數屬性時,會沿著作用域鏈一層層往上查找(沿著VO往上找),如果一直找到全局對象中還沒有該變數屬性,就會報錯未定義;
-
上文中提到了很多概念名詞,下面來總結一下:
名詞 解釋 ECS 執行上下文棧(Execution Context Stack),也可稱為調用棧,以棧的形式調用創建的執行上下文 GEC 全局執行上下文(Global Execution Context),在執行全局程式碼前創建 FEC 函數執行上下文(Functional Execution Context),在執行函數前創建 VO Variable Object,早期ECMA規範中的變數環境,對應Object VE Variable Environment,最新ECMA規範中的變數環境,對應環境記錄 GO 全局對象(Global Object),解析全局程式碼時創建,GEC中關聯的VO就是GO AO 函數對象(Activation Object),解析函數體程式碼時創建,FEC中關聯的VO就是AO












