首先是對運行時上下文和調用棧的關係及其運行過程的理解:
運行時上下文理解爲運行時的當前運行環境,而JavaScript代碼的執行是執行棧來完成,執行棧又稱爲調用棧,後面一律稱爲調用棧。調用棧正如其名字一樣是一個LIFO的棧結構,即後進先出。
我們把調用棧的運行理解爲上下文的進出棧過程,即先入棧的上下文就後出棧。
例子:
var 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');
// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
上述代碼運行時(即調用first函數時)我們理解爲首先全局上下文最先入棧,其次是first函數 的上下文,最後是second函數的上下文入棧,而三個上下文環境的出棧順序正好相反,second函數會最先執行完成並且其上下文出棧,其次是first函數執行完成並且上下文出棧,最後是全局上下文出棧。
運行時上下文的創建:
執行上下文的創建
執行上下文分兩個階段創建:1)創建階段; 2)執行階段
創建階段
-
1、確定 this 的值,也被稱爲 This Binding。
-
2、LexicalEnvironment(詞法環境) 組件被創建。
-
3、VariableEnvironment(變量環境) 組件被創建。
直接看僞代碼可能更加直觀
ExecutionContext = {
ThisBinding = <this value>, // 確定this
LexicalEnvironment = { ... }, // 詞法環境
VariableEnvironment = { ... }, // 變量環境
}
This Binding
-
全局執行上下文中,
this
的值指向全局對象,在瀏覽器中this
的值指向window
對象,而在nodejs
中指向這個文件的module
對象。 -
函數執行上下文中,
this
的值取決於函數的調用方式。具體有:默認綁定、隱式綁定、顯式綁定(硬綁定)、new
綁定、箭頭函數,具體內容會在後面【對JavaScript中this的理解】部分詳解。
詞法環境(Lexical Environment)
詞法環境有兩個組成部分
-
1、環境記錄:存儲變量和函數聲明的實際位置
-
2、對外部環境的引用:可以訪問其外部詞法環境
詞法環境有兩種類型
-
1、全局環境:是一個沒有外部環境的詞法環境,其外部環境引用爲 null。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,
this
的值指向這個全局對象。 -
2、函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了
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>
}
}
變量提升的原因:
在創建階段,函數聲明存儲在環境中,而變量會被設置爲 undefined
(在 var
的情況下)或保持未初始化(在 let
和 const
的情況下)。所以這就是爲什麼可以在聲明之前訪問 var
定義的變量(儘管是 undefined
),但如果在聲明之前訪問 let
和 const
定義的變量就會提示引用錯誤的原因。這就是所謂的變量提升。
執行階段
此階段,完成對所有變量的分配,最後執行代碼。
如果 Javascript 引擎在源代碼中聲明的實際位置找不到 let
變量的值,那麼將爲其分配 undefined
值。
對於變量提升的詳細理解:
即在代碼執行是首先會執行上下文的創建,然後在分配變量並且執行代碼。
所以在創建上下文的階段我們看到let和const以及函數在函數上下文環境或者全局上下文環境中的詞法環境中被綁定,而var變量是在變量環境中被綁定到上下文的。在創建完成後,let和const的狀態爲未初始化狀態,而var爲undefined的狀態,我們在執行階段纔會給各個變量分配值。而在執行階段,我們看到如果在使用某個let或const修飾的變量之前沒有聲明,我們這時給它賦值,因爲在詞法環境中沒有這個變量的引用,那麼會提示引用錯誤,因爲我們能夠區分未初始化和undefined的;而如果是var修飾的變量,因爲在創建上下文完成後本是undefined,在聲明之前訪問 var
定義的變量也是,其表現是一致的,因此不做區分。
如有理解錯誤,還望不吝指正!!!
參考: