大家好,本系列挑戰文希望可以把 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 loop
、functional programming
特性 等基礎,讓讀者能夠有足量的能力離開新手村!
今天介紹的 Execution Context 是其中一個 JavaScript 的核心機制,因為會把一層層 JS 執行的最小單位 frame
push 進 stack,因此也叫做 stack frame。EC 這個機制將會影響與決定
⋯⋯若不知道以上的內容也請先不要走開,今天的文章將一一解釋 😉 (後來發現篇幅太長了,因此今天只有提到前面兩點,後半段下回繼續)
當我們開始執行 JavaScript 時,每段程式碼都是在某個 EC 中進行的。無論是函數呼叫、全域程式碼(Global Code)、或 eval
,都會創建自己的 Execution Context。每個 EC 會有:
var
、函式宣告let
or const
this
Binding
eval
是一個 JS 的內建函數,可以執行字串形式的 JS script。
JavaScript 是靜態作用域語言,意思是變數的作用域在編譯時就決定了,取決於程式碼的書寫位置,而不是根據呼叫位置。當執行時,要查找變數,JS 引擎只會從當前 EC 的 Lexical Environment 查找。以下提供一個簡單的例子:宣告一個 function 並在內部宣告另一個 function,讓我們來觀察模擬 JS 是如何逐步 trace down:
當程式開始執行時,首先建立 Global EC。在最一開始的 建立階段(Creation Phase) 建立全域物件、變數以及函數:
- 全域物件(window
or global
),以及 this
- 變數有 a
,初始化為 undefined
- 函數有 outer
,初始化為 function object
接著,在 執行階段(Execution Phase) 給變數賦值
- a = 10
EC Stack 裡此時只有 Global EC
[Global EC]
outer()
→ 創建 Outer EC當我們呼叫 outer()
後,便把 outer EC push 進 EC stack,所以此時的 EC stack 有 Global 和 Outer stack 兩個。此步驟同樣的會先有 建立階段:
- 變數 b
→ undefined
- 函數 inner
→ function object
執行階段:
- b = 20
- 呼叫 inner()
,建立 Inner EC
EC Stack 已經依序被 push 進 Outer EC 和 Inner EC
[Inner EC]
[Outer EC]
[Global EC]
inner()
→ 創建 Inner ECc
→ undefined
c = 30
console.log(a, b, c)
這邊的 console.log
會根據 stack 裡面的順序,一一沿著作用域鏈 (Scope Chain) 層層往下查找變數的值:
a
→ 在 Inner 沒有 → Outer 沒有 → Global 找到 10
b
→ Inner 沒有 → Outer 找到 20
c
→ Inner 找到 30
得到最終的輸出:
10 20 30
總結上述的例子,詞法作用域 (Lexical Scope) 就是在函數定義時記住外部環境,不管函數被怎麼調用,它始終會沿著「定義時的作用域鏈」向上查找變數。
Hoisting(提升)是 JavaScript 中的一種行為機制,指的是 變數宣告(var)和函式宣告會被「提升」到其所在作用域的最前面。 無論把變數或函式宣告寫在程式碼的哪裡,JavaScript 會先在執行之前把這些宣告「往上移動」。
1. 函式提升(Function Hoisting)
函式宣告會被整個提升,包含函式本體。
2. 變數提升(Variable Hoisting)
使用 var
宣告的變數會被提升,但只有宣告本身會被提升,初始化不會。像是以下範例中 var a
的宣告被提升到了程式碼的最上方,但 a = 10
的賦值還是在原本的位置。因此,變數在宣告之前被使用時,值是 undefined
,而不是錯誤。
let
和 const
不會被提升到可用狀態
let
和 const
也會被引擎知道,但在程式碼執行到宣告之前,變數處於暫時性死區(Temporal Dead Zone, TDZ),會拋出錯誤,跟上面的栗子相比,這種寫法可以幫助我們更好偵錯,最終維持程式的品質與可預測性。這正是為什麼現代 JavaScript 開發中建議盡量用 let
和 const
而避免用 var
。
var
的四大理由1. var
的 Hoisting 行為容易讓人誤解var
宣告的變數會被提升,但初始化不會,導致在宣告前使用變數時得到 undefined
,這很容易讓人誤會變數已經有值,實際卻不是。這會增加 Debug 難度,也容易造成潛在的程式錯誤。
2. var
只有函式作用域,沒有塊級作用域(block scope)
var
在函式內宣告後,會在整個函式內都有效,即使在 {}
內宣告也沒意義,容易造成變數覆寫、污染。相比之下,let
和 const
有塊級作用域,只在 {}
內有效,更容易控制變數範圍。
3. let
和 const
有 Temporal Dead Zone (TDZ)
使用 let
或 const
在變數宣告之前訪問會拋錯,幫助提早發現錯誤,讓程式碼更安全。
4. const
明確表達不變的意圖
用 const
宣告的變數不可以被重新賦值,讓變數使用更具語意。
所以我們把前面查找變數的例子再次拿出來,不知道讀者剛剛有沒有注意到,例子中宣告變數的方法是 var
而非平常較常用的 let
和 const
。如果我們改為以下
雖然這邊的結果會是一樣的,不過細節上有些許差異:
1. 作用域let
和 const
是塊級作用域(block scope),作用範圍是最近的大括號 {}
內:這裡因為 a
是全域,b
在 outer
函式內,c
在 inner
內,所以行為才會和 var
一樣。
2. Hoisting 與暫時性死區 (Temporal Dead Zone, TDZ)var
會提升 (hoist)變數宣告,初始化為 undefined
;let
和 const
也會被提升,但不會初始化,而是進入 TDZ,直到程式碼執行到該行才會被賦值。但這邊沒有先呼叫才賦值的情況,所以安全過關~
當我們認識了 EC 這個機制跟他的衍生小夥伴後,寫出來的程式將更穩定、更能被正確預測。最後,讓我們來列點總結今天提到的內容:
JavaScript execution model | MDN