javascript的執行上下文
這是一個非常抽象的概念,你無需徹底的弄明白它的意思,你只需要明白它做了什麼。
在充分理解他做了什麼之前還是要了解一下它到底是什麼
Execution Context(執行上下文)是 ECMA-262 標準中定義的一個抽象概念,用於同 Executable Code(可執行程式碼)進行區分。
1:什麼是執行程式碼----Executable Code
合法的,可以被直譯器解析執行的程式碼。
分為三類
- Global Code:全域性程式碼
- Function Code:函式體內的程式碼
- Eval Code:使用 eval() 函式執行的程式碼
2:什麼是執行上下文----Execution Context
執行上下文 是 ES 用來跟蹤程式碼執行狀態和相關資源集合的特殊機制。它決定了執行程式碼執行的過程中可以訪問的資料。
每當 Javascript 程式碼在執行的時候,它都是在執行上下文中執行。
分為三類
- Global Execution Context:全域性執行上下文
這是預設或者說基礎的上下文,任何不在函式內部的程式碼都在全域性上下文中執行。它會執行兩件事:建立一個全域性的 window 物件(瀏覽器的情況下),並且設定 this 的值等於這個全域性物件。一個程式中只會有一個全域性執行上下文。
- Function Execution Context:函式執行上下文
每當一個函式被呼叫時, 都會為該函式建立一個新的上下文。每個函式被呼叫時都有它自己的執行上下文。函式上下文可以有任意多個。每當一個新的執行上下文被建立,它會按定義的順序(將在後文討論)執行一系列步驟。
- Eval Execution Context:eval() 函式執行上下文
由於 JavaScript 開發者並不經常使用 eval,所以在這裡我不會討論它。
3:執行上下文的基本工作方式
先理解兩個名詞:執行上下文棧(Execution Context Stack)、執行執行上下文(Running Execution Context)
執行上下文棧( Execution Context Stack ):用來儲存所有執行上下文的棧,是一種擁有 LIFO(後進先出)資料結構的棧。 當 JavaScript 引擎第一次遇到你的指令碼時,它會建立一個全域性的執行上下文並且壓入當前執行棧。每當引擎遇到一個函式呼叫,它會為該函式建立一個新的執行上下文並壓入棧的頂部。引擎會執行那些執行上下文位於棧頂的函式。當該函式執行結束時,執行上下文從棧中彈出,控制流程到達當前棧中的下一個上下文。
執行執行上下文( Running Execution Context ):正在使用的執行上下文。在任意時間,最多隻能有一 個正在執行程式碼的執行上下文。
4:基本工作方式
執行執行上下文總是在執行上下文棧的頂部,全域性執行上下文總在執行上下文棧的底部。無論什麼時候,只要控制權從與當前執行執行上下文相關的可執行程式碼上切換到另一部分與當前執行執行上下文不相關的可執行程式碼上,一個新的執行上下文就會被建立,新建立的執行上下文會被放在當前的執行執行上下文的上面,成為新的執行執行上下文。
5:具體工作流程
如前言中提到的,ES 標準中並沒有從技術實現的角度定義執行上下文準確型別和結構,為了更方便地解釋 執行程式碼和執行上下文之間的關係,暫且用陣列表示執行上下文棧,然後用虛擬碼來操作執行上下文棧:
DCStack = [] // 執行上下文棧
<1:開始執行程式碼:全域性執行程式碼與全域性執行上下文
解析器在解析執行程式碼時首先執行全域性程式碼,為其建立對應的執行上下文,全域性上下文被壓入執行上下文棧
ECStack = [
globalContext // 全域性執行上下文
]
<2:開始執行函式:函式程式碼與函式執行上下文
注意:函式程式碼中不包括內部函式的程式碼
執行下面的函式
(function foo(bar) {
if (bar) {
return
}
foo(true);
})()
我們用虛擬碼還原一下執行棧中發生了什麼??
// 第一次呼叫 foo
ECStack = [
<foo> functionContext,
globalContext
]
// 第二次呼叫 foo
ECStack = [
<foo> functionContext – recursively(遞迴),
<foo> functionContext,
globalContext
]
我們看一個實際的例子
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');
首先執行這段程式碼,解析器解析到了這段程式碼,於是先建立了一個全域性上下文,並把全域性上下文壓入執行棧
ECStack = [
Global Context
]
然後解析器檢測到了 first(),開始呼叫first函式,於是建立了一個first函式上下文,並把這個函式向下文壓入到執行棧的頂部(一般執行棧的頂部都是正在執行的上下文,現在正在呼叫first函式,所以頂部就是他的上下文)
ECSstack= [
First Function Context-----(頂部是正在執行的上下文)
Global Context
]
在first() 函式內部又呼叫了second()函式,於是JavaScript 引擎為second()函式建立了一個屬於他的執行上下文,並把它壓入執行棧的最頂部。(因為現在執行second()函式,所以他的執行上下文就在最頂部,因為first()函式沒有執行完所以他的執行上下文依然在執行棧的佇列中)
ECSstack = [
Cecond Function Context-----(頂部是正在執行的上下文)
First Function Context
Global Context
]
執行完second()函式之後,它的執行上下文會自動從執行棧彈出,並且控制流程執行下一個執行上下文,即 first() 函式的執行上下文。
ECSstack= [
First Function Context-----(頂部是正在執行的上下文)
Global Context
]
當 first() 執行完畢,它的執行上下文自動從棧彈出,控制流程按順序到達全域性執行上下文。一旦所有程式碼執行完畢,JavaScript 引擎從當前棧中移除全域性執行上下文。
ECStack = [
Global Context
]
6:JavaScript 引擎是怎麼建立執行上下文?
建立執行上下文有兩個階段:1>:建立階段 和 2>:執行階段。
1>:建立階段–(The Creation Phase)
在建立階段會發生三件事
ExecutionContext = {
ThisBinding = <this value>, // this繫結
LexicalEnvironment = { ... }, // 詞法環境
VariableEnvironment = { ... }, // 變數環境
}
- This 繫結。
在全域性執行上下文中,this 的值指向全域性物件。(在瀏覽器中,this引用 Window 物件)。在函式執行上下文中,this 的值取決於該函式是如何被呼叫的。如果它被一個引用物件呼叫,那麼 this 會被設定成那個物件,否則 this 的值被設定為全域性物件或者 undefined(在嚴格模式下)。例如:
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因為 'baz' 被物件 'foo' 呼叫
let bar = foo.baz;
bar(); // 'this' 指向全域性 window 物件,因為沒有指定引用物件
- 建立詞法環境元件。
詞法環境是一種規範型別,基於 ECMAScript 程式碼的詞法巢狀結構來定義識別符號和具體變數和函式的關聯。一個詞法環境由環境記錄器和一個可能的引用外部詞法環境的空值組成。
有點沒明白
簡單來說詞法環境是一種定義識別符號以及變數的巢狀結構。(這裡的識別符號指的是變數/函式的名字,而變數是對實際物件[包含函式型別物件]或原始資料的引用)。
在詞法環境的內部有兩個部件組成:
1:環境記錄器:是儲存變數和函式宣告的實際位置。
:2: 外部環境的引用:意味著它可以訪問其父級詞法環境(作用域)。
詞法環境有兩種型別:
1:全域性環境:(在全域性執行上下文中)是沒有外部環境引用的詞法環境。全域性環境的外部環境引用是 null。它擁有內建的 Object/Array/等、在環境記錄器內的原型函式(關聯全域性物件,比如 window 物件)還有任何使用者定義的全域性變數,並且 this的值指向全域性物件。
2:函式環境:函式內部使用者定義的變數儲存在環境記錄器中。並且引用的外部環境可能是全域性環境,或者任何包含此內部函式的外部函式。
環境記錄器也有兩種型別:
1:宣告式環境記錄器儲存變數、函式和引數。
2:物件環境記錄器用來定義出現在全域性上下文中的變數和函式的關係。
簡而言之,
環境記錄器在全域性環境中,環境記錄器是物件環境記錄器。 在函式環境中,環境記錄器是宣告式環境記錄器。
注意
函式環境,宣告式環境記錄器還包含了一個傳遞給函式的 arguments 物件(此物件儲存索引和引數的對映和傳遞給函式的引數的length)
抽象地講,詞法環境在虛擬碼中看起來像這樣:
GlobalExectionContext = { // 全域性執行上下文
LexicalEnvironment: { // 詞法環境元件
EnvironmentRecord: { // 環境記錄器 ---物件環境記錄器
Type: "Object",
// 在這裡繫結識別符號
}
outer: <null> // 外部環境引用, 是沒有外部環境引用的詞法環境。全域性環境的外部環境引用是 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 = {
1:ThisBinding: <Global Object>, //this繫結
2: LexicalEnvironment: { // 詞法環境 --全域性的詞法環境
EnvironmentRecord: { //環境記錄器
Type: "Object",
// 在這裡繫結識別符號
a: < uninitialized >, // 變數a的繫結(let)
b: < uninitialized >, // 變數b 的繫結(const)
multiply: < func > // 函式宣告
}
outer: <null> // 外部環境的引用nul
},
3: VariableEnvironment: { // 變數環境 --全域性的詞法環境
EnvironmentRecord: { //環境記錄器
Type: "Object",
// 在這裡繫結識別符號
c: undefined, // 變數c 的繫結(var)
}
outer: <null> // 外部環境的引用nul
}
}
// 函式的執行上下文-----(只有遇到呼叫函式 multiply 時,函式執行上下文才會被建立)
FunctionExectionContext = {
1:ThisBinding: <Global Object>, // this 繫結
2:LexicalEnvironment: { //詞法環境 --函式的詞法環境
EnvironmentRecord: { // 環境記錄器
Type: "Declarative",
// 在這裡繫結識別符號
Arguments: {0: 20, 1: 30, length: 2}, // 宣告式環境記錄器還包含了一個傳遞給函式的 arguments 物件(此物件儲存函式引數鍵值對和傳遞給函式的引數的length)。
},
outer: <GlobalLexicalEnvironment> // 外部環境的引用是全域性環境
},
3:VariableEnvironment: { //變數環境
EnvironmentRecord: { // 環境記錄器
Type: "Declarative",
// 在這裡繫結識別符號
g: undefined // 變數g的繫結(var)
},
outer: <GlobalLexicalEnvironment> // 外部環境的引用是全域性環境
}
}
可能你已經注意到 let 和 const 定義的變數並沒有關聯任何值,但 var 定義的變數被設成了 undefined。 這是因為在建立階段時,引擎檢查程式碼找出變數和函式宣告,雖然函式宣告完全儲存在環境中,但是變數最初設定為 undefined(var 情況下),或者未初始化(let 和 const 情況下)。 這就是為什麼你可以在宣告之前訪問 var 定義的變數(雖然是 undefined),但是在宣告之前訪問 let 和 const 的變數會得到一個引用錯誤。 這就是我們說的變數宣告提升。