JavaScript的執行上下文定義
簡單定義執行上下文是評估和執行JavaScript代碼的環境的抽象概念。每當JavaScript代碼在運行的時候,它都是在執行上下文中運行。
執行上下文的類型
- 全局執行上下文=》默認基礎的上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:創建一個全局的window對象(瀏覽器情況下),並且設置this的值等於這個全局對象。一個程序中只會有一個全局執行上下文中。
- 函數執行上下文 =》每當一個函數被調用時,都會爲該函數創建一個新的上下文。每個函數都有它自己的執行上下文,不過是在函數被調用時創建的。函數上下文可以有任意多個。每當一個新的執行上下文被創建,它會按定義的順序執行一系列步驟。
- Eval函數執行上下文 =》執行在eval函數內部的代碼也會有它屬於自己的執行上下文,但JavaScript開發者不經常使用eval,不討論
執行棧
執行棧,也就是所說的“調用棧”,是一種擁有LIFO後進先出數據結構的棧,被用來存儲代碼運行時創建的所有執行上下文。也叫執行上下文棧【管理執行上下文】
// 爲了模擬執行上下文棧的行爲,讓我們定義執行上下文棧是一個數組:
let ECStack = [];
// 試想當 JavaScript 開始要解釋執行代碼的時候,最先遇到的就是全局代碼,所以初始化的時候首先就會向執行上下文棧壓入一個全局執行上下文,我們用 globalContext 表示它,並且只有當整個應用程序結束的時候,ECStack 纔會被清空,所以程序結束之前, ECStack 最底部永遠有個 globalContext:
ECStack = [
globalContext
];
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
third()
}
function third(){
console.log("third function");
}
first();
console.log('Inside Global Execution Context');
// 當執行一個函數的時候,就會創建一個執行上下文,並且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。知道了這樣的工作原理,讓我們來看看如何處理上面這段代碼:
/*
// 僞代碼
// first()
ECStack.push(<first> functionContext);
// first中竟然調用了second,還要創建second的執行上下文
ECStack.push(<second> functionContext);
// 擦,second還調用了third!
ECStack.push(<third> functionContext);
//third執行完畢
ECStack.pop();
// second執行完畢
ECStack.pop();
// first執行完畢
ECStack.pop();
// javascript接着執行下面的代碼,但是ECStack底層永遠有個globalContext
*/
/*
Inside first function
js執行上下文.js:15 Inside second function
js執行上下文.js:19 third function
js執行上下文.js:11 Again inside first function
js執行上下文.js:23 Inside Global Execution Context
*/
怎麼創建執行上下文
創建執行上下文有兩個階段:1、創建階段【進入執行上下文】 2、執行階段【代碼執行】
1、創建階段
在JavaScript代碼執行前,執行上下文將經歷創建階段。在創建階段會發生三件事:
- this值得決定,即我們所熟知得this綁定
- 創建詞法環境組件
- 創建變量環境組件
this綁定:
全局執行上下文中,this的值指向全局對象【在瀏覽器中,this引用window對象】;
函數執行上下文中,this的值取決於該函數是如何調用的。引用對象調用,指向這個引用對象;全局調用指向全局對象window或者underfined(在嚴格模式下);new實例對象指向這個new對象;call、bind、apply調用指向call、bind、apply函數的第一個參數對象;
詞法環境
變量環境
進入執行上下文時,還沒有執行代碼,變量對象會包括:
1、函數的所有形參(如果是函數上下文)
- 由名稱和對象值組成的一個變量對象的屬性被創建[var/let/const定義變量],如 var a = 1;let b=2
- 沒有實參,屬性值設爲【underfined】,let 定義變量沒有先聲明就引用會報錯,沒有initialization
2、函數聲明
- 由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建
- 如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性
3、變量聲明
- 由名稱和對應值(undefined)組成一個變量對象的屬性被創建;,let 定義變量沒有先聲明就引用會報錯,沒有initialization
- 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
//看一個代碼案例:
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>
}
}
注意 — 只有遇到調用函數 multiply 時,函數執行上下文才會被創建。
可能你已經注意到 let 和 const 定義的變量並沒有關聯任何值,但 var 定義的變量被設成了 undefined。
這是因爲在創建階段時,引擎檢查代碼找出變量和函數聲明,雖然函數聲明完全存儲在環境中,但是變量最初設置爲 undefined(var 情況下),或者未初始化(let 和 const 情況下)。
這就是爲什麼你可以在聲明之前訪問 var 定義的變量(雖然是 undefined),但是在聲明之前訪問 let 和 const 的變量會得到一個引用錯誤。
這就是我們說的變量聲明提升。
2、執行階段
在此階段完成對所有這些變量的分配,最後執行代碼。
注意 — 在執行階段,如果 JavaScript 引擎不能在源碼中聲明的實際位置找到 let
變量的值,它會被賦值爲 undefined
。