iT邦幫忙

2025 iThome 鐵人賽

DAY 1
0
Modern Web

JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天系列 第 1

離開 JS 初階工程師新手村的 Day 01|冒險開始:執行上下文 Execution Context

  • 分享至 

  • xImage
  •  

大家好,本系列挑戰文希望可以把 JS 稍微進階一些的技術原理、React 的進階常用應用、以及一些工程開發上會遇到的狀況,如重構、design pattern等,由淺入深的介紹一遍。幫助有需要提升 JS 能力的各位新手工程師,包含我自己,免於害怕有朝一日會被 AI 給取代的威脅!

我去年想寫 Three.js 結果失敗了 XD 只撐了五天,希望這一次 可以多幾天 能夠順利盡量把 30 天的份量寫完!!

~以下正文開始~


💡本篇主題與重點字:**Execution Context**
- Scope Chain
- Hoisting

本週的主題將著重於 JavaScript 的一些奇幻魔法,在剛開始學的時候可能不會注意到的細節,或是一不小心就錯過學習的核心機制!筆者第一個學ㄉ程式語言是 C++,JS 總是邊做 project 邊學,因此直到現在有時寫 JS 或做 code review 的時候還是會「蛤?這樣也可以?!」。
希望透過介紹 Execution Context閉包原型鏈event loopfunctional programming特性 等基礎,讓讀者能夠有足量的能力離開新手村!


今天介紹的 Execution Context 是其中一個 JavaScript 的核心機制,因為會把一層層 JS 執行的最小單位 frame push 進 stack,因此也叫做 stack frame。EC 這個機制將會影響與決定

  1. 變數查找與作用域解析
  2. 程式執行的順序與 Hoisting 機制
  3. this 綁定
  4. 非同步處理與 Event Loop

⋯⋯若不知道以上的內容也請先不要走開,今天的文章將一一解釋 😉 (後來發現篇幅太長了,因此今天只有提到前面兩點,後半段下回繼續)

Execution Context 基礎概念

當我們開始執行 JavaScript 時,每段程式碼都是在某個 EC 中進行的。無論是函數呼叫、全域程式碼(Global Code)、或 eval ,都會創建自己的 Execution Context。每個 EC 會有:

  • Variable Environment(變數環境)—— 儲存當前作用域中的 var 、函式宣告
  • Lexical Environment(詞法環境)—— 儲存當前作用域中的變數、函式宣告、let or const
  • Scope Chain(作用域鏈)—— 當前 EC 和所有外層 EC 組成,用於變數查找
  • this Binding

eval 是一個 JS 的內建函數,可以執行字串形式的 JS script。

模擬 JavaScript 引擎的變數查找和函數呼叫

JavaScript 是靜態作用域語言,意思是變數的作用域在編譯時就決定了,取決於程式碼的書寫位置,而不是根據呼叫位置。當執行時,要查找變數,JS 引擎只會從當前 EC 的 Lexical Environment 查找。以下提供一個簡單的例子:宣告一個 function 並在內部宣告另一個 function,讓我們來觀察模擬 JS 是如何逐步 trace down:

https://ithelp.ithome.com.tw/upload/images/20250911/20168365IzYhx5zzKG.png

Step 1:全域執行上下文(Global Execution Context)

當程式開始執行時,首先建立 Global EC。在最一開始的 建立階段(Creation Phase) 建立全域物件、變數以及函數:
- 全域物件(window or global),以及 this
- 變數有 a,初始化為 undefined
- 函數有 outer,初始化為 function object
接著,在 執行階段(Execution Phase) 給變數賦值
- a = 10

EC Stack 裡此時只有 Global EC

[Global EC]

Step 2:呼叫 outer() → 創建 Outer EC

當我們呼叫 outer() 後,便把 outer EC push 進 EC stack,所以此時的 EC stack 有 Global 和 Outer stack 兩個。此步驟同樣的會先有 建立階段
- 變數 bundefined
- 函數 inner → function object
執行階段
- b = 20
- 呼叫 inner(),建立 Inner EC

EC Stack 已經依序被 push 進 Outer EC 和 Inner EC

[Inner EC]
[Outer EC]
[Global EC]

Step 3:呼叫 inner() → 創建 Inner EC

  • 建立階段
    • 變數 cundefined
  • 執行階段
    • c = 30
    • 執行 console.log(a, b, c)

這邊的 console.log會根據 stack 裡面的順序,一一沿著作用域鏈 (Scope Chain) 層層往下查找變數的值:

  1. 查找 a → 在 Inner 沒有 → Outer 沒有 → Global 找到 10
  2. 查找 b → Inner 沒有 → Outer 找到 20
  3. 查找 c → Inner 找到 30

得到最終的輸出:

10 20 30

總結上述的例子,詞法作用域 (Lexical Scope) 就是在函數定義時記住外部環境,不管函數被怎麼調用,它始終會沿著「定義時的作用域鏈」向上查找變數。


Hoist

Hoisting(提升)是 JavaScript 中的一種行為機制,指的是 變數宣告(var)和函式宣告會被「提升」到其所在作用域的最前面。 無論把變數或函式宣告寫在程式碼的哪裡,JavaScript 會先在執行之前把這些宣告「往上移動」。

1. 函式提升(Function Hoisting)
函式宣告會被整個提升,包含函式本體。

https://ithelp.ithome.com.tw/upload/images/20250911/20168365FIEaghiybg.png

2. 變數提升(Variable Hoisting)
使用 var 宣告的變數會被提升,但只有宣告本身會被提升,初始化不會。像是以下範例中 var a 的宣告被提升到了程式碼的最上方,但 a = 10 的賦值還是在原本的位置。因此,變數在宣告之前被使用時,值是 undefined,而不是錯誤。

https://ithelp.ithome.com.tw/upload/images/20250911/201683658dHSiBBavL.png

letconst 不會被提升到可用狀態
https://ithelp.ithome.com.tw/upload/images/20250911/20168365qGl8wphfF0.png

letconst 也會被引擎知道,但在程式碼執行到宣告之前,變數處於暫時性死區(Temporal Dead Zone, TDZ),會拋出錯誤,跟上面的栗子相比,這種寫法可以幫助我們更好偵錯,最終維持程式的品質與可預測性。這正是為什麼現代 JavaScript 開發中建議盡量用 letconst而避免用 var

珍惜生命,遠離 var 的四大理由

1. var 的 Hoisting 行為容易讓人誤解
var 宣告的變數會被提升,但初始化不會,導致在宣告前使用變數時得到 undefined,這很容易讓人誤會變數已經有值,實際卻不是。這會增加 Debug 難度,也容易造成潛在的程式錯誤。

2. var 只有函式作用域,沒有塊級作用域(block scope)
https://ithelp.ithome.com.tw/upload/images/20250911/20168365WzVuWt0Aat.png

var 在函式內宣告後,會在整個函式內都有效,即使在 {} 內宣告也沒意義,容易造成變數覆寫、污染。相比之下,letconst 有塊級作用域,只在 {} 內有效,更容易控制變數範圍。

3. letconst 有 Temporal Dead Zone (TDZ)
使用 letconst 在變數宣告之前訪問會拋錯,幫助提早發現錯誤,讓程式碼更安全。

4. const 明確表達不變的意圖
const 宣告的變數不可以被重新賦值,讓變數使用更具語意。


所以我們把前面查找變數的例子再次拿出來,不知道讀者剛剛有沒有注意到,例子中宣告變數的方法是 var 而非平常較常用的 letconst。如果我們改為以下

https://ithelp.ithome.com.tw/upload/images/20250911/20168365uF2A3VdbUt.png

雖然這邊的結果會是一樣的,不過細節上有些許差異:

1. 作用域
letconst塊級作用域(block scope),作用範圍是最近的大括號 {} 內:這裡因為 a 是全域,bouter 函式內,cinner 內,所以行為才會和 var 一樣。

2. Hoisting 與暫時性死區 (Temporal Dead Zone, TDZ)
var 會提升 (hoist)變數宣告,初始化為 undefinedletconst 也會被提升,但不會初始化,而是進入 TDZ,直到程式碼執行到該行才會被賦值。但這邊沒有先呼叫才賦值的情況,所以安全過關~


總結

當我們認識了 EC 這個機制跟他的衍生小夥伴後,寫出來的程式將更穩定、更能被正確預測。最後,讓我們來列點總結今天提到的內容:

  1. 變數查找與作用域解析
    • JS 執行時會先建立執行 Context,並根據它建立 Scope Chain。
    • 變數與函數如何被找到,完全依賴執行時的 EC 與 Scope Chain。
  2. 程式執行的順序與 Hoisting 機制
    • 每當函數被呼叫,一個新的執行上下文會被 push 進 Call Stack。
    • 在 EC 創建階段,會先進行 Hoisting,把變數與函數的宣告提到最上面,這解釋了為何可以在宣告前呼叫 function。

參考資料

JavaScript execution model | MDN


下一篇
離開 JS 初階工程師新手村的 Day 02|閉!殺!技!:閉包 Closure
系列文
JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言