一、執行上下文
簡單的來說,執行上下文是一種對Javascript
程式碼執行環境的抽象概念,也就是說只要有Javascript
程式碼執行,那麼它就一定是執行在執行上下文中
執行上下文的型別分為三種:
- 全域性執行上下文:只有一個,瀏覽器中的全域性物件就是
window
物件,this
指向這個全域性物件 - 函式執行上下文:存在無數個,只有在函式被呼叫的時候才會被建立,每次呼叫函式都會建立一個新的執行上下文
- Eval 函式執行上下文: 指的是執行在
eval
函式中的程式碼,很少用而且不建議使用
下面給出全域性上下文和函式上下文的例子:
紫色框住的部分為全域性上下文,藍色和橘色框起來的是不同的函式上下文。只有全域性上下文(的變數)能被其他任何上下文訪問
可以有任意多個函式上下文,每次呼叫函式建立一個新的上下文,會建立一個私有作用域,函式內部宣告的任何變數都不能在當前函式作用域外部直接訪問
二、生命週期
執行上下文的生命週期包括三個階段:建立階段 → 執行階段 → 回收階段
建立階段
建立階段即當函式被呼叫,但未執行任何其內部程式碼之前
建立階段做了三件事:
- 確定 this 的值,也被稱為
This Binding
- LexicalEnvironment(詞法環境) 元件被建立
- VariableEnvironment(變數環境) 元件被建立
虛擬碼如下:
ExecutionContext = { ThisBinding = <this value>, // 確定this LexicalEnvironment = { ... }, // 詞法環境 VariableEnvironment = { ... }, // 變數環境 }
This Binding
確定this
的值我們前面講到,this
的值是在執行的時候才能確認,定義的時候不能確認
詞法環境
詞法環境有兩個組成部分:
-
全域性環境:是一個沒有外部環境的詞法環境,其外部環境引用為
null
,有一個全域性物件,this
的值指向這個全域性物件 -
函式環境:使用者在函式中定義的變數被儲存在環境記錄中,包含了
arguments
物件,外部環境的引用可以是全域性環境,也可以是包含內部函式的外部函式環境
虛擬碼如下:
GlobalExectionContext = { // 全域性執行上下文 LexicalEnvironment: { // 詞法環境 EnvironmentRecord: { // 環境記錄 Type: "Object", // 全域性環境 // 識別符號繫結在這裡 outer: <null> // 對外部環境的引用 } } FunctionExectionContext = { // 函式執行上下文 LexicalEnvironment: { // 詞法環境 EnvironmentRecord: { // 環境記錄 Type: "Declarative", // 函式環境 // 識別符號繫結在這裡 // 對外部環境的引用 outer: <Global or outer function environment reference> } }
變數環境
變數環境也是一個詞法環境,因此它具有上面定義的詞法環境的所有屬性
在 ES6 中,詞法環境和變數環境的區別在於前者用於儲存函式宣告和變數( let
和 const
)繫結,而後者僅用於儲存變數( var
)繫結
舉個例子
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30);
執行上下文如下:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { // 詞法環境 EnvironmentRecord: { Type: "Object", // 識別符號繫結在這裡 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { // 變數環境 EnvironmentRecord: { Type: "Object", // 識別符號繫結在這裡 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 識別符號繫結在這裡 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 識別符號繫結在這裡 g: undefined }, outer: <GlobalLexicalEnvironment> } }
留意上面的程式碼,let
和const
定義的變數a
和b
在建立階段沒有被賦值,但var
宣告的變數從在建立階段被賦值為undefined
這是因為,建立階段,會在程式碼中掃描變數和函式宣告,然後將函式宣告儲存在環境中
但變數會被初始化為undefined
(var
宣告的情況下)和保持uninitialized
(未初始化狀態)(使用let
和const
宣告的情況下)
這就是變數提升的實際原因
執行階段
在這階段,執行變數賦值、程式碼執行
如果 Javascript
引擎在原始碼中宣告的實際位置找不到變數的值,那麼將為其分配 undefined
值
回收階段
執行上下文出棧等待虛擬機器回收執行上下文
二、執行棧
執行棧,也叫呼叫棧,具有 LIFO(後進先出)結構,用於儲存在程式碼執行期間建立的所有執行上下文
當Javascript
引擎開始執行你第一行指令碼程式碼的時候,它就會建立一個全域性執行上下文然後將它壓到執行棧中
每當引擎碰到一個函式的時候,它就會建立一個函式執行上下文,然後將這個執行上下文壓到執行棧中
引擎會執行位於執行棧棧頂的執行上下文(一般是函式執行上下文),當該函式執行結束後,對應的執行上下文就會被彈出,然後控制流程到達執行棧的下一個執行上下文
舉個例子:
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context');
轉化成圖的形式
簡單分析一下流程:
- 建立全域性上下文請壓入執行棧
first
函式被呼叫,建立函式執行上下文並壓入棧- 執行
first
函式過程遇到second
函式,再建立一個函式執行上下文並壓入棧 second
函式執行完畢,對應的函式執行上下文被推出執行棧,執行下一個執行上下文first
函式first
函式執行完畢,對應的函式執行上下文也被推出棧中,然後執行全域性上下文- 所有程式碼執行完畢,全域性上下文也會被推出棧中,程式結束
參考文獻
- https://zhuanlan.zhihu.com/p/107552264
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。