keywords:Tokenizing
,Parsing
,Code-Generation
,scope
,LHS
,RHS
,ReferenceError
,TypeError
撰寫程式碼時我們經常用到變數存取資料,但那些變數存在於何處?以及我們程式是如何在需要的時候找到它們的 🤔
範疇 ( scope )
以一組定義良好的規則來將變數儲存在某些位置,以便之後找回那些變數。這組規則就稱作 範疇 ( scope )
編譯器理論
JavaScript 一般被歸類為動態的 ( dynamic ) 或直譯式 ( interpreted ) 語言,但實際上是一種編譯式語言 ( compiled language ) ,與傳統編譯式語言不同的是,它並非事先就先編譯好,幾乎就是編譯後即刻執行。
傳統的編譯式語言在執行之前通常會經歷三個步驟,大略稱為『 編譯 』:
- Tokenizing 語法基本單元化 & Lexing 語彙分析 :將一串字元拆解成有意義的組塊,這些組塊叫做『 語法基本單元化 tokens 或稱語彙單元 』
- Parsing 剖析或語法分析:接受由 語法基本單元( tokens )所構成的串流或陣列,並將之轉為一種元素內嵌的樹狀結構,整個陣列代表了程式的文法結構,這種樹狀結構稱為『 AST , abstract syntax tree 抽象語法樹 』
- Code-Generation 產生目的程式碼:接受一個 AST 並將之轉為可執行程式碼的過程。這部分會隨著語言及目標平台等因素的不同而有所變化,簡單的說就是有一種方式可以把 AST 轉為一組機器指令,實際建立出一個叫做 a 的變數( 包括保留記憶體等步驟 ),並將一個儲存到 a 中。
Esprima 是可以看到一段程式碼編譯過程的 Tokens and Syntax,上方例子連結在此
JavaScript 引擎所進行的工作比傳統的三個單純的步驟複雜很多,對 JavaScript 來說,編譯過程在程式碼被執行前通常僅以微秒來計算。
範疇及它的好朋友們
- Engine 引擎:負責從開始到結束的編譯程序,並執行 JavaScript 程式
- Compiler 編譯器:處理剖析與程式碼產生的所有苦工
- Scope 範疇:負責收集及維護所有已宣告的識別字( 即變數 )所構成的查找清單 ( look-up list),並強制施加一組嚴格的規則,規範這些變數對於目前正在執行的程式碼,是否可以取用
查找動作種類,當 Engine 要對一個變數進行查找時,有兩種方法:
- LHS ( lefthand side ):取得指定的目標
RHS ( righthand side ):取回它的來源值
let a = 9; let b = a;
這邊總共有 2 個 LHS 以及 1 個 RHS
LHS:取得指定的目標,我們有兩個地方需要 Engine 幫我找到指定的目標,就是在指定a = 9; b = a;
時
RHS:取回它的來源值,當b = a;
時,Engine 需要找到 a 裡面放了什麼
Engine 與 Scope 的對話
function f1(x) {
let y = x;
return x + y + z;
}
let z = 1;
f1( 9 );
Engine:嘿 Scope 我有一個對 z 的 LHS ,請問你知道它嗎?
Scope:I Know. Compiler 剛剛才宣告它,它是一個變數
Engine:Thx. 我現在要把 1 指定給 z
Engine:嘿 Scope 我有一個對 f1 的 RHS ,請問你知道它嗎?
Scope:I Know ok. Compiler 剛才宣告它,它是一個函式請拿去用吧
Engine:感謝感謝,我正在執行 f1 呢
Engine:拍謝 Scope 我有一個 x 的 LHS 參考,你知道嗎?
function f1(x) 其實會隱含地指定 9 給 x ( x = 9 ),進行 LHS 查找動作
Scope:Of Course, Compiler 最近把它宣告為 f1 的一個形式參數,拿去唄
Engine:非常感謝,現在該把 9 指定給 x 了
Engine:Yo Scope 我有一個對 y 的 LHS 及 x 的 RHS ,請問你知道它們嗎?
Scope:I always Know. Compiler 剛才宣告 y ,它是一個變數,x 就是跟剛剛同一個變數,拿去吧
Engine:Thank you. 我現在要把 x 的值指定給 y
Engine:Hi f1 裡面的 Scope ,有聽過 z 嗎?我需要它的一個 RHS 參考
f1's Scope:I don't know sorry.
Engine:It's ok.
( Engine 只好跑去問 f1 外面的 Scope )
Engine:Yo Scope 請問你聽過 z 嗎? 我需要它的一個 RHS 參考
Scope:有啊,我有拿去用吧, By the way 我叫作全域範疇
巢狀範疇
Engine 會從正在執行程式碼的範疇中開始尋找目標變數,如果沒找到就會持續往上一層找,直到全域範疇,不管有沒有找到搜尋動作就會停止。
一個比喻範疇尋找目標的情境:假設你有一棟透天厝,你的朋友在附近想找你吃個熱炒喝兩杯,他跟你說他要去接你而他人就快到你家的巷子口了,叫你趕快準備準備。
你匆匆忙忙的從 4 樓的房間穿好衣服跑到 1 樓,準備要把門鎖上時發現,OMG 我錢包在房間啊!於是你開始從 1 樓的客廳開始找有沒有平時放在客廳桌上的紙鈔,如果 1 樓找不到就得去 2 樓的工作房找,如果最後 1~3 樓都找不到錢,那只好回 4 樓房間拿了。
- 範疇就是你家的每一層樓,變數就是紙鈔
錯誤
為何要區分 RHS 及 LHS?
先來看兩個例子:
function f1(x) {
console.log( x + y);
y = x;
}
f1(9); // ReferenceError: y is not defined
----------------------------------------------
function f2(x) {
y = x;
}
f2(9);
y; // 9
- 當 RHS 查找動作在巢狀 scopes 中找到一個變數,它找不到 y,引擎就會擲出一個 ReferenceError
- 當 LHS 查找動作到達全域範疇後,依然找不到目標變數,在不是 Strict Mode 中執行時,全域範疇就會在全域變數中自動創建 那個找不到的變數名稱為新變數,並把它交給 Engine
如果在 Strict Mode 中 LHS 查找動作到達全域範疇後,依然找不到目標變數,就會擲出一個 ReferenceError
ReferenceError 與範疇解析動作失敗有關
TypeError 則是範疇的解析動作成功了,但試圖對結果進行非法或不可能的動作,像是調用一個非函式值,或是在 null 及 undefined 值上參考一個特性
let x = 9; x(); // TypeError: x is not a function null.a; // TypeError: Cannot read property 'a' of null