# [JavaScript] 執行環境(Execution Context)與堆疊(Stack) ###### tags: `前端筆記` ## Execution Context(以下簡稱 EC) 就是人類寫給 JavaScript 的任務(代碼),JavaScript 把任務分成兩個種類(type): **1. Global Execution Context(全域執行環境)** - 所有沒有在函式內的代碼都屬於 Global Execution Context(以下簡稱 GE) - 最先被建立的環境,同一個時間只會存在一個 GE - 屬於 GE 的代碼「在 JavaScript 檔案中任何一處都可以被讀取」 - 會把 `this` 的值綁定為 `global object`(在瀏覽器中就是 `window`) **2. Functional Execution Context(函式執行環境)** - 所有在函式內的代碼就是屬於 Functional Execution Context(以下簡稱FE) - **每次函式被叫用(is invoked)才會被建立出來的環境** - 負責處理函式內的任務(也就是代碼) - 每個 FE 都是獨立的,換句話說,即便在是一個函式包另一個函式情況下,兩個 FE 還是分開的,不過內層函式可以讀取外層函式 ```javascript= function outer () { const a = 1; console.log('Hi'); inner(); function inner () { console.log(a); } } outer(); // Hi // 1 ``` ## Calling Stack(呼叫堆疊) 當檔案被讀取且經編譯後,JavaScript 會把人類寫的任務(代碼)按照順序執行,負責這個部分的就是 Calling Stack。首先 JavaScript 會在 Calling Stack 先建立 GE,之後就會一行一行地執行任務,如果遇到函式,就會建立 FE,並把該 FE 放在 Calling Stack 的最上面,然後會暫停處理 GE 的任務,開始處理該 FE 的任務,處理完畢就會把該 FE 移出(pop off),繼續處理 GE 的任務,等到 GE 的任務全部執行完畢, Calling Stack 就會被清空。(通常就是當頁面被關閉的時候) **這種先處理後面的任務就是大家常說的「後進先出」(LIFO => Last in, First out)** ```javascript= let me = 'Lun' function firstFunc () { console.log('我在第一個函式裡面'); secondFunc(); console.log('我還在第一個函式裡面'); } function secondFunc() { console.log('我在第二個函式裡面'); } firstFunc(); console.log('我在 GE 裡'); ``` 1. 建立 GE 並執行 ![](https://i.imgur.com/cHuciK9.png) 2. firstFunc 被叫用時創造 FE 並執行,此時 GE 的任務會被暫停 ![](https://i.imgur.com/SL7KAeT.png) 3. secondFunc 被叫用時創造 FE 並執行,此時 firstFunc 的任務會被暫停 ![](https://i.imgur.com/O7ynUpY.png) 4. secondFunc 的 FE 執行完畢,被移出 Calling Stack ![](https://i.imgur.com/yJ6Jaj1.png) 5. firstFunc 的 FE 執行完畢,被移出 Calling Stack,最後 GE 被執行完畢也會被移出 Calling Stack ![](https://i.imgur.com/pyaW76i.png) 輸出結果 ![](https://i.imgur.com/r91iG94.png) ## 那 Execution Context 到底是如何被創造(create)的呢? 根據上面的流程已經知道 JavaScript 是怎麼安排任務順序執行人類代碼的了,現在可以探討背後的運作情形。 **每一個 Execution Context 都有兩個階段 (1)創造階段(Creation Stage)以及(2)執行階段(Execution Stage)。** Execution Context 的結構會是這樣子([*ref. :Lexical Environment — The hidden part to understand Closures*](https://amnsingh.medium.com/lexical-environment-the-hidden-part-to-understand-closures-71d60efac0e0)): ```javascript= // Execution context in ES5 ExecutionContext = { ThisBinding: <this value>, // 註:這邊先不談 this 的指向,之後會另外整理 VariableEnvironment: { ... }, LexicalEnvironment: { ... } } ``` ### 1. 創造階段(Creation Stage) 在創造階段會執行以下的事情: - VariableEnvironment 是一個內部保存該 EC 的變數、 function declarations(函式宣告式,以下簡稱FD)以及 FD 的參數(arguments)的值 - VariableEnvironment 有兩個屬性: 1. environmentRecord:用來保存該 EC 的變數、FD 以及 FD 的參數 => 如果是用 `var` 宣告的變數,在此時會被初始化而且值會先變成 `undefined` => 如果是用 `const ` 以及 `let` 宣告的變數,此時就不會初始化,也不會給值 `undefined` => FD 的參數在創造階段就會被帶進一個叫 `arguments` 的物件之中,並且會直接把該值給 FD 裡面對應的引數位置 2. outer reference:用來讀取外層執行環境 => 在自己的 EC 找不到該變數的話就會往外層捕捉(capture),會一直往外層找直到 GE - `this` 的值(也就是指向)會被決定 - LexicalEnvironment 會拷貝 VariableEnvironment 的東西 => 所以 LexicalEnvironment 也會有 environmentRecord 及 outer reference 這兩個屬性 ```javascript= function func1 (x) { console.log(x); console.log(b); var b = 10; console.log(b); } func1(5); // func1 的 Functional Execution Context FunctionExectionContext = { Phase: 'Creation', ThisBinding: <this value>, // 先不談 this VariableEnvironment = { environmentRecord : { // FD 會有的參數物件 {對應順序(index): value, 物件 property 的長度, 屬於哪個 FD} arguments : {0: 10, length: 1, callee: func1}, x: 10, b: undefined // var 宣告的變數會初始化並給值 undefined }, outer: <GE> }; ``` ### 2. 執行階段(Execution Stage) 結束創造階段就會進入執行階段(代碼的執行順序是一行一行由上至下執行),此時會做以下的事情: - 變數會被賦值(也就是變數會與值的記憶體連結) => environmentRecord 內的變數此時會與值的記憶體位址連結 - 在創建階段往外層捕捉(capture)到的值在執行階段可以立即被使用 ```javascript= function func1 (x) { console.log(x); console.log(b); var b = 10; console.log(b); } func1(5); // 5 // undefined => 變數 b 是用 var 關鍵字宣告的變數,所以這時候 b 還沒有與值 10 的記憶體連結,會回傳創造階段初始化的 undefined // 10 => 因為已經與值 10 的記憶體連結,所以會印出 10 // func1 的 Functional Execution Context FunctionExectionContext = { Phase: 'Execution', ThisBinding: <this value>, // 先不談 this VariableEnvironment = { environmentRecord : { // FD 會有的參數物件 {對應順序(index): value, 物件 property 的長度, 屬於哪個 FD} arguments : {0: 10, length: 1, callee: func1}, x: 10, b: 10 // 此時 b 會與值為 10 的記憶體連結 }, outer: <GE> }; ``` ## 更多範例 ### 1. FD 會提升(Hoisting)的原理 使用 FD 就可以互相叫用函式,不會因為順序的先後造成錯誤,因為在 GE 創造階段時就會保存 FD 的所有代碼,且進入 FE 的時候也會先創建再執行。 ```javascript= function func1 () { const a = 1; func2(a); } function func2 (a) { console.log(a); } func1(); ``` ```javascript= 1. GE 先建立(Creation Stage) GlobalEnvironment = { Phase: 'Creation', ThisBinding: <this value>, // 註:這邊先不談 this 的指向,之後會另外整理 VariableEnvironment: { environmentRecord: { func1 : <func> // 直接保存該 FD 內的代碼 func2 : <func> // 直接保存該 FD 內的代碼 }, outer: null // GE 沒有 outer reference }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; 2. GE 被執行(Execution Stage)=> Calling Stack 開始處理 func1() 3. 所以 Calling Stack 會創造 FE FunctionExectionContext = { Phase: 'Creation', VariableEnvironment: { environmentRecord: { a: <uninitialized> // => 因為是用 const 宣告的變數 }, outer: <GE> }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; 4. 建造完畢進入執行階段,結果發現要執行另一個函式,Calling Stack 開始處理 func2() FunctionExectionContext = { Phase: 'Execution', VariableEnvironment: { environmentRecord: { a: 1 // 執行階段會讓變數與值的記憶體位置連結 }, outer: <GE> }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; 5. 所以 Calling Stack 就會建造 func2() 的 FE FunctionExectionContext = { Phase: 'Creation', VariableEnvironment: { environmentRecord: { arguments : {0: 1, length: 1, callee: func2}, a: 1 }, outer: <GE> }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; 6. func2() 進入執行階段 FunctionExectionContext = { Phase: 'Execution', VariableEnvironment: { environmentRecord: { arguments : {0: 1, length: 1, callee: func2}, a: 1 }, outer: <GE> }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; 7. func2 的 FE 執行完畢 Calling Stack 刪除 func2 的 FE 8. func1 的 FE 執行完畢 Calling Stack 刪除 func1 的 FE 9. GE 繼續執行,直到頁面關閉 ``` ### 2. `let` 跟 `const` 所宣告的變數也會提升(Hoisting),只是行為與 `var` 不同 `let` 及 `const` 會有 `TDZ - Temporal Dead Zone`。 簡單來說:`let` 及 `const` 也會有提升(Hoisting),但是不會像是 `var` 一樣會被初始化並給值 `undefined`。在 `let` 以及 `const` 與值的記憶體連結以前讀取(使用)該變數就會出現錯誤。 [*ref. :我知道你懂 hoisting,可是你了解到多深?*](https://blog.techbridge.cc/2018/11/10/javascript-hoisting/) ```javascript= var a = 10 function test(){ console.log(a) let a } test() // 不是顯示 10 // 而是 Uncaught ReferenceError: Cannot access 'a' before initialization // 哪泥!!!!!???? ``` ```javascript= 1. GE 先建立(Creation Stage) GlobalEnvironment = { Phase: 'Creation', ThisBinding: <this value>, // 註:這邊先不談 this 的指向,之後會另外整理 VariableEnvironment: { environmentRecord: { a: undefined // 因為是 var,所以初始化值 undefined test : <func> // 直接保存該函式內的代碼 }, outer: null // GE 沒有 outer reference }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; 2. GE 被執行(Execution Stage)=> Calling Stack 開始處理 test() 3. 所以 Calling Stack 會創造 FE FunctionExectionContext = { Phase: 'Creation', VariableEnvironment: { environmentRecord: { a: <uninitialized> // => 因為是用 const 宣告的變數 }, outer: <GE> }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; 4. test() 進入執行階段 FunctionExectionContext = { Phase: 'Execution', VariableEnvironment: { environmentRecord: { // 在連結值 1 以前有執行 console.log(a) // 因為 FD 裡確實有 a(即便狀態是 <uninitialized>),所以 JavaScript 還是會找自己 FE 裡面的東西,並不會往外找。但因為 let 及 const 沒有辦法在與值連結前被讀取,所以就會丟出錯誤 a: 1 }, outer: <GE> }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 }; ``` ### 3. FD 傳進來的參數可以被更改 在執行階段參數可以被更改,更改後還是會遵守 Function Scope 的規則,所以出了函式 `y` 便不存在了。 ```javascript= function x (y) { console.log(y); y = '我改變了嗎?' console.log(y); } x(10); // 10 // 我改變了嗎? console.log(y); // Uncaught ReferenceError: y is not defined ``` ```javascript= FE = { Phase: 'Creation', ThisBinding: <this value>, // 註:這邊先不談 this 的指向,之後會另外整理 VariableEnvironment: { environmentRecord: { arguments : {0: 10, length: 1, callee: x}, y: 10; }, outer: GE }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 } FE = { Phase: 'Execution', ThisBinding: <this value>, // 註:這邊先不談 this 的指向,之後會另外整理 VariableEnvironment: { environmentRecord: { arguments : {0: 10, length: 1, callee: x}, // y: 10; y: '我改變了嗎?' }, outer: GE }, LexicalEnvironment: {...} // 拷貝 VariableEnvironment 的東西 } ``` ## 總結 1. Calling Stack 的規則是後進先出(LIFO - Last in, first out) 2. 每個 EC 都有兩個階段:**(1)創造階段(Creation Stage)以及(2)執行階段(Execution Stage)** 3. 創造階段初始化變數、FD、FD 的 arguments,執行階段變數與值的記憶體連結 4. `var` 在創造階段會被初始化並給值 `undefined`,但是 `let` 及 `const` 則不會被初始化,所以如果在 `let` 及 `const` 與記憶體連結以前叫用會出錯 5. 函式在創造階段就會讀取參數,並把在它帶入函式內對應引數的位置 6. 全部的函式在創造階段時就會開始往外層找需要的變數(其實是要找值的記憶體,因為在函式內部自己找不到),之後在執行階段就可以拿來直接使用 - 順序:自身的 FE 內先找,找不到就會透過 outer reference 讀取外層的 EC 找,會一路找到 GE - 連 GE 都找不到就會報錯 8. LexicalEnvironment 就是以 VariableEnvironment 為藍圖拷貝,所以 LexicalEnvironment 跟 VariableEnvironment 都有 environmentRecord 及 outer reference 的屬性 - 其實它們還是有些許的不同,但是我真的不了解到底是什麼不同,之後變強再回來看這篇[ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.](http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-2-lexical-environments-ecmascript-implementation/#environment-record-types) 以上是我自己理解的觀念,如果有錯誤再麻煩指證,多謝。 ## 參考文章 1. [[JavaScript] Javascript 的執行環境 (Execution context) 與堆疊 (Stack)](https://medium.com/itsems-frontend/javascript-execution-context-and-call-stack-e36e7f77152e) 2. [我知道你懂 hoisting,可是你了解到多深?](https://blog.techbridge.cc/2018/11/10/javascript-hoisting/) 3. [【修正模型】4-1 執行上下文(Execution Context)](https://ithelp.ithome.com.tw/articles/10253457) 4. [Understanding Execution Context and Execution Stack in Javascript](https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0) 5. [Lexical Environment — The hidden part to understand Closures](https://amnsingh.medium.com/lexical-environment-the-hidden-part-to-understand-closures-71d60efac0e0) 6. [Udemy: The Complete JavaScript Course 2021: From Zero to Expert!](https://www.udemy.com/course/the-complete-javascript-course/) Section: 8 => 對應的章節 7. [JavaScript Visualizer A tool for visualizing Execution Context, Hoisting, Closures, and Scopes in JavaScript](https://ui.dev/javascript-visualizer/) => 大神做的超猛工具,可以視覺化顯示 EC 的步驟