【課程筆記】JS#3:JavaScript的執行原理,Javascript 的執行環境 (Execution context) 與執行堆疊 (Call Stack)

 

JavaScript,Execution_context,call_stack

這一系列文章,是在Udemy上參與 Jonas  Schmedtmann的課程-The Complete JavaScript Course 2021: From Zero to Expert!所做的學習筆記,為了深入理解JavaScript運作原理,記下那些我未曾注意過的JavaScript細節。內容若有任何錯誤,歡迎留言交流指教!


Outline
- Javascript 的執行環境 (Execution context)
-執行環境(Execution context) 分為全域執行環境、函式執行環境
-建立執行環境 ( creation phase )
-執行階段( execution phase ) 以執行堆疊Call Stack追蹤執行

在上篇,我們了解到JavaScript的運行環境及及不同程式碼編譯方法的差別後,現在讓我們更深入了解JavaScript的執行機制。


首先,在介紹JavaScript引擎時,我們知道主要是由兩個元件所組成:

1.Call Stack (堆疊棧)
2.Memory Heap (內存堆)

而我們知道所謂 Call Stack 是程式碼實際執行的地方,是透過執行環境(Execution Contexts)來執行程式碼。那所以,執行環境到底是什麼、又是如何運作的呢?

讓我們簡單複習一下JavaScript的即時編譯過程 ─
JavaScript原始碼進入引擎 → 解析(Parsing) → 轉為數據結構,組成抽象語法樹(AST) → 編譯(Compilation) → AST轉換為machine code並立即被執行【Execution Contexts】 → program running

現在,假設我們的程式碼已經完成了編譯(Compilation),進入執行階段 ─ 首先會為頂層程式碼(Top Level Code)創建所謂全域執行環境,也就是會首先執行函式以外的所有程式碼,(因為函式只有在被調用時才會被執行,所以很合理吧?)

*頂層程式碼(Top Level Code)就是最外層,也就是不存在函式中的程式碼。

現在讓我們來看一下範例:
//全域變數
const name = 'emily'

//expression function
const first = () => {
let a = 1
const b = second()
a = a + b
return a
}

//declaration function
function second(){
var c = 2
return
}

首先,從這段程式碼中,我們可以看到name、first函式、second函式都屬於頂層程式碼,只是函式需要等候被調用才會執行,所以在這個範例的全域執行環境中,只有name正在被執行。

現在,問題繞回來了,所以執行環境到底是什麼啊?

§ 執行環境 (Execution Context) §

也被稱為執行上下文、執行背景空間、執行情境。簡單來說,執行環境就是一個對應於當前所需要被執行的部分程式碼而準備的環境,而在這個環境中儲存了所有執行這部份程式碼所需要的一切資訊,例如將區域變數或參數傳入函式中。換言之,也就是JavaScript的程式碼永遠只在執行環境中被執行。

白話一點比喻,就像是你去必勝客買一份獨享披薩套餐,你領到餐點時,袋子裡面會有紙巾、叉子、披薩、還有其他醬料等,妳想吃披薩,要透過這個袋子裡的物件才能享用它。而這一整個套餐袋子就是執行環境(Execution Context),裏頭的叉子、紙巾、醬料則是吃披薩所需要的工具,而披薩就是要執行的程式碼。

所以執行環境可以有很多個囉?是、也不是。

◆ 全域執行環境 (Global Execution Context) 只會有一個!

全域執行環境也是預設環境(Default Context),無論Javascript的項目有多大,都只會有一個全域執行環境,會在進入執行程序時,首先被創建出來,也就是為頂層程式碼所創建第一環境。而在上述的例子中,全域執行環境裡的對象就是name、first函式、second函式(但記得,函式還沒被執行,因為沒被調用。)

◆ 函式執行環境 (Function Execution Context)個別函式專屬的執行環境

記得先前提到,Javascript永遠都會在EC內被執行嗎?也就是說當函式被調用時(invoke)將會各別為該函式建立專屬的執行環境,也就是說有多少個函式要被執行,就有多少個函式執行環境。

而以上所有 EC 的集合,就組成了先前提到的 Call Stack (執行堆疊、堆疊棧)。並同時,當所有以上執行環境都執行完畢後,JavaScript引擎還是持續保持待機狀態,等待回調函式(callback function)例如滑鼠事件所觸發的函式抵達,而這中間會經過事件回調(Event Loop)的協調機制進行。

大致了解後面的拼圖後,現在我們再回到執行環境 (Execution Context) 中細細拆解。
執行程式碼主要分為兩個階段,創建環境與執行階段(creation & execution phase)。

§ 創建執行環境 ( creation phase ) ─ 執行環境裡到底有什麼?§

1.變數環境 Variable Environment

let,const,var 變數宣告
Function 聲明
arguments object 參數物件

其中參數物件包含所有傳入該函式的一切參數。而顧名思義,函式執行環境是為了特定函式被執行而建立的,這也就表示當執行結束,這個變數環境也就結束了。

2.建立外部環境(Outer Environment) ─ 範疇鏈 Scope Chain

首先,全域執行環境相當單純,當然就沒有所謂 Outer Environment,因為他本身就是最外部了。所以此項主角肯定是函式執行環境!

函式理所當然可以訪問內部的一切變數(這沒問題),但同時也因為範圍鏈(Scope Chain)的作用而讓函式可以訪問「外部變數」,也就是創造了外部環境。

範疇鏈的意思就是所有的內部作用區域,都可以訪問他們的外部作用區域,也就是子作用域可以往父層作用域查找所需要的變數。例如 function a 包在 function b 裡,function b 就是 function a 的外部環境。詳細部份會在後續獨立筆記中記錄。

bbb
bab
bbb

3.特殊變數 this keyword

this會在執行環境創建時,自然被生成。

*注意細節:箭頭函式 Arrow 沒有this、也不會有參數物件。但同樣的,函式執行環境會因為範疇鏈的作用,而擁有外部環境,所以雖然箭頭函式本身不具有this與參數物件,但他仍可以使用距離最近的父層常規函式的this與參數物件。這部份也會在this獨立筆記中記錄。

現在,統合以上基礎常識,讓我們回到範例來拆解個別執行環境
const name = 'emily'

const first = (){
let a = 1
const b = second(7,9)
a = a + b
return a
}

function second(){
var c = 2
return
}
const x = first()

以下是創建出來的個別執行環境:全域執行環境(預設創建)、與兩個函式執行環境

Global first() secons()
name = 'emily'
first = (function)
second = (function)
x = (未知)*註一 
a = 1 
b = (未知) 
需執行second()
c = 2
參數 [7,9]
*註二

註一:嚴格來說,在創建執行環境階段時,數值都是未能得知的,因為需要經過執行才知道,所以在這裡寫下未知的值,其實是有瑕疵的,只是因為要模擬執行環境的建立過程,先不談到執行,所以才先這樣寫下。

註二:這裡參數傳入的是陣列[7,9],而這在常規函式是允許的,但在箭頭函式則是不允許的。

透過以上的範例可以看到EC創建的邏輯,現在讓我們想像,如果有上百個EC,那JS引擎要如何追蹤執行先後順序?而這就是執行堆疊(Call Stack)的任務了!

§ 執行堆疊 (調用棧) Call Stack §

超白話來說,執行堆疊就是堆疊執行環境的地方,用來追蹤我們目前所在程式碼的位置,所以最上層的執行環境EC,就是我們目前正在執行的程式碼片段,而當執行完畢,該執行環境就會被從執行堆疊中彈出。

現在讓我們回到範例中,解釋執行堆疊的運作過程。

const name = 'emily'

const first = (){
let a = 1
const b = second(7,9)
a = a + b
return a
}

function second(){
var c = 2
return
}
const x = first()

開始執行(模擬Call Stack變化):

1.
建立預設
全域執行環境 
2. 
X 須執行first()
於是堆疊first EC
3.
first()執行到變數b
需要執行second()
故堆疊second EC
4.
second執行完彈出
繼續回到first
5.
first執行完彈出
X終於被賦值 




 
    second()    
  first() first()first() 
Global EC Global EC Global EC Global EC Global EC

流程解釋:
  1. 首先頂層代碼需要首先被執行,所以創建了全域執行環境,並被放入執行堆疊中。
  2. 當程式碼跑到X,調用first函式,first專屬的執行環境就會被放入執行堆疊中,開始執行
  3. 而當first函式中執行到第2行 const b = second(7,9) 時,需要調用second函式,second專屬的執行環境就會從first上方被堆疊上去,而second函式開始執行(此時已經沒有需要堆疊的執行環境了)
  4. second函式執行完後該執行環境彈出,將值回傳給first的b變數,此時first函式終於回到原本優先執行的位子,並開始執行,執行完後彈出
  5. 最後X得到回傳值,終於被賦值完畢。執行堆疊回到原本只有全域執行環境的狀態。

最後,執行雖然乍看結束了,但目前執行程序仍然會保持在全域執行環境狀態,直到他真正被結束的時候,也就是當我們關閉瀏覽器或視窗時,在那時候就會連全域執行環境都從執行堆疊中彈出。

這樣一路看下來,我們會很清楚知道,執行堆疊對於單執行序的JavaScript作用的重點核心,就在於追蹤執行,因為沒有了執行堆疊,我們就無法追蹤現在程式馬執行到哪裡,或應該回到哪裡。

執行堆疊就像是JS的地圖,讓JS執行順序不會迷失。以上就是執行環境、執行堆疊的工作原理內容若有任何錯誤,歡迎留言交流指教!

接下來,讓我們進一步了解範圍鏈 (Scope Chain),也就是函式執行環境中,所謂的外部環境到底是什麼意思。

延伸閱讀:



留言

這個網誌中的熱門文章

【南投│白毫禪寺】日式禪風的建築外型,卻是一部妙法蓮華經的具體呈現

【緬甸】緬甸自助旅行,一個人旅行不負責任懶人包:行程、住宿、換匯、簽證、交通、安全小提醒