[JavaScript] Javascript 的執行環境 (Execution context) 與堆疊 (Stack)

itsems
itsems_frontend
Published in
8 min readOct 1, 2020
Photo by Vanessa von Wieding on Unsplash
Outline
- 程式碼到底是怎麼跑的? How your programs run ?
- 執行環境是什麼? What's Execution Context ?
- 建立執行環境的兩個階段 (creation & execution phase)
- 執行堆疊 (Execution Stack / Call Stack)

程式碼到底是怎麼跑的?

在講執行環境之前,先溫習一下更基本的觀念。

我們寫的程式碼,到底是怎麼跑在電腦或瀏覽器上的?如果電腦只收 1 跟 0,為什麼我們寫的英文也可以跑? 還是其實電腦也可以理解英文字母 a-z 嗎?

不是,其實還有一個東西,介於我們和電腦之間,稱為編譯器 (Compiler),編譯器的任務,就是一個字一個字的讀你的程式碼 (Source code),決定你的語法是否有效,接著轉換為電腦看得懂的機器碼 (Machine code)。

精準一點說, Compiler 是一種電腦程式,他會將我們寫的原始程式 (source program / source language) 輸入,翻譯過後產生電腦看得懂的目標語言 (target language),最後打包為執行檔給電腦執行。原始程式通常是高階語言 (High-level language) 如 C#、Go、Java 等。

中間的機器人,就是 compiler

那 Javascript 也是這樣運行的嗎? 可以說是,

程式語言又分為編譯語言 (Compiled language)直譯語言 (Interpreted language) 兩種,

  • 編譯語言
    通常是靜態語言 (static language) 如 C、Rust…等。會在運行前先定義好型別或型別檢查等等,效能較好,通常是可以獨立執行的,就像我們平常常看到的 .exe 執行檔。
  • 直譯語言
    通常為動態語言 (dynamic language) 如 Javascript、Python...等。在型別處理上較有彈性,相對速度也會比編譯語言慢一些,需要執行環境才能執行。

Javascript 屬於直譯語言,並同樣有直譯器 (Interpreter) 來解讀他。

直譯器和編譯器運作上最主要的差異,就是編譯器簡單說是將整包程式碼翻譯成機器碼之後再讓電腦執行,而直譯器則是一行一行的轉換為機器碼讓電腦執行,這樣的差異會造成編譯語言在除錯上會比較緩慢,不像是直譯語言可以開發完一小段就馬上執行。

直譯語言以 Javascript 為例,需要跑在 Javascript 引擎 (engine) 上才能運行。每一個瀏覽器都有自己的 Javascript 引擎,如 Chrome 的 v8,或是 Firefox 的 SpiderMonkey。這些 Javascript 引擎都是為了同一件事情,將你的 Javascript 轉換為電腦懂的機器碼,來運行你的程式。

基本的觀念差不多溫習到這邊,接下來再來談談 Javascript 的執行環境。

執行環境 (Execution Context) 是什麼?

又可稱為執行背景空間、執行情境,可以說是由執行環境來決定現在要執行的程式是誰。

Javascript 共會建立兩種執行環境:

  1. 全域執行環境 (Global Execution Context)
    在執行任何程式之前,預設會建立的一個全域環境。
    全域執行環境替我們做了兩件事情:
    a. 建立全域物件 (Global object),以 web browser 來說就是 window
    b. 建立「this」
  2. 函式執行環境 (Function Execution Context)
    在函式執行 (invoke) 的時候會各別為函式建立專屬的執行環境,
    執行一個函式就會建立一個該函式的執行環境,所以有可能同時會有多個函式執行環境。

建立執行環境的兩個階段 (creation & execution phase)

在執行環境建立的時候,會有兩個階段:

創建階段 (Creation Phase)

在創建階段的時候,會做幾件事情:

  • 建立全域物件和「this」
    全域物件只有在全域執行環境的時候才會參照到,this 則是一定會建立。
  • 建立外部環境 (Outer Environment)
    對於全域執行環境而言,就沒有 Outer Environment,對於函式執行環境而言,如果 function b 包在 function a 裡面,那 function b 的外部環境就是 function a
  • 空出給變數和函式的記憶體空間:Hoisting 提升
有點忘記提升是什麼的話,可以參考這裡:
[JavaScript] Javascript 中的 Hoisting(提升):幫你留位子

執行階段 (Execute Phase)

在這個階段,Javascript 就開始一行一行讀你的 code 了,如果在這個環境下找不到需要的參數,就會繼續往外層、往全域一層一層的去找,如果正常的執行了,那麼這個執行環境就會跳出 (pop off) 執行堆疊 (stack) 中。

執行堆疊 (Execution Stack / Call Stack)

現在我們的執行環境建好了,接下來看看執行環境該怎麼運行。

Javascript 是一種「單執行續 (single-thread)」的語言,意思就是一次只能做一件事情,如果安排了很多事情要給他做,他就會讓這些事情去排隊,再一件一件做。

並且這些排隊的函式,會是「後進先出」的概念,以下面這段程式為例:

function a() {
b();
}
function b() {
console.log('hi');
}
a();
a();

運行上面這段 code 的時候,執行堆疊會像是這個樣子:

接著來看看在 devtool 中實際上的樣子,將這段 code 放進程式碼後 run 在頁面上,打開 chrome 的 devtool 選擇 source 頁籤中的你的 js 檔案:

在程式碼行數 9 的位置的左邊點一下,會出現如圖的藍色箭頭,這是中斷點 (Breakpoints) 的意思,意思是當程式跑到這行的時候會停止。設定好中斷點後再重整我們的頁面:

如上所述,程式停在我們設定的 a() 函式這行,但是右邊的 Call Stack 出現了剛剛沒有的 (anonymous),這就是全域執行環境

接下來我們往下面一步看,點一下這個向下箭頭

點了一下,程式開始執行 a() 函式內容,藍底線跑進 a() 函式裡面,並在 Call Stack 中看見 a() 函式執行環境建立了,再下一步:

程式開始執行 b() 函式內容,並在 Call Stack 中看見 b() 函式執行環境建立在 a 執行環境上面,再下一步:

執行位置跑到 function b 的 } 外括弧位置,表示 b 函式執行結束:

執行位置跑到 function a 的 } 外括弧位置,表示 a 函式執行結束。且 Call Stack 中函式 b 已經跳出堆疊,消失在堆疊中,恢復至 a 函式執行環境

再下一步則程式再往下一行執行,第一個 a 函式跳出 Call Stack,恢復全域執行環境。

上面的圖用連續動作看一次:

理解了執行環境和執行堆疊之後,就可以接著看一個 Javascript 中也很重要的觀念:範圍鏈 (Scope Chain)。

內容若有任何錯誤,歡迎留言交流指教! 🐏

ref:

wiki-編譯器
編譯系統設計
第1天:一文搞懂直譯與編譯語言的差異
編譯語言 VS 直譯語言
Javascript Execution Context and Hoisting
JavaScript 閉包與範圍 ── Execution Context
JavaScript 全攻略:克服 JS 的奇怪部分
JavaScript Execution Context

--

--

itsems
itsems_frontend

Stay Close to Anything that Makes You Glad You are Alive.