[31] 範疇 - 編譯三步驟、巢狀範疇、錯誤


keywords:Tokenizing,Parsing,Code-Generation,scope,LHS,RHS,ReferenceError,TypeError
撰寫程式碼時我們經常用到變數存取資料,但那些變數存在於何處?以及我們程式是如何在需要的時候找到它們的 🤔

範疇 ( scope )

以一組定義良好的規則來將變數儲存在某些位置,以便之後找回那些變數。這組規則就稱作 範疇 ( scope )

編譯器理論

JavaScript 一般被歸類為動態的 ( dynamic ) 或直譯式 ( interpreted ) 語言,但實際上是一種編譯式語言 ( compiled language ) ,與傳統編譯式語言不同的是,它並非事先就先編譯好,幾乎就是編譯後即刻執行。

傳統的編譯式語言在執行之前通常會經歷三個步驟,大略稱為『 編譯 』:

  1. Tokenizing 語法基本單元化 & Lexing 語彙分析 :將一串字元拆解成有意義的組塊,這些組塊叫做『 語法基本單元化 tokens 或稱語彙單元 』
    tokens
  2. Parsing 剖析或語法分析:接受由 語法基本單元( tokens )所構成的串流或陣列,並將之轉為一種元素內嵌的樹狀結構,整個陣列代表了程式的文法結構,這種樹狀結構稱為『 AST , abstract syntax tree 抽象語法樹 』
    syntax
  3. Code-Generation 產生目的程式碼:接受一個 AST 並將之轉為可執行程式碼的過程。這部分會隨著語言及目標平台等因素的不同而有所變化,簡單的說就是有一種方式可以把 AST 轉為一組機器指令,實際建立出一個叫做 a 的變數( 包括保留記憶體等步驟 ),並將一個儲存到 a 中。

Esprima 是可以看到一段程式碼編譯過程的 Tokens and Syntax,上方例子連結在此

JavaScript 引擎所進行的工作比傳統的三個單純的步驟複雜很多,對 JavaScript 來說,編譯過程在程式碼被執行前通常僅以微秒來計算。

範疇及它的好朋友們

  • Engine 引擎:負責從開始到結束的編譯程序,並執行 JavaScript 程式
  • Compiler 編譯器:處理剖析與程式碼產生的所有苦工
  • Scope 範疇:負責收集及維護所有已宣告的識別字( 即變數 )所構成的查找清單 ( look-up list),並強制施加一組嚴格的規則,規範這些變數對於目前正在執行的程式碼,是否可以取用
  • 查找動作種類,當 Engine 要對一個變數進行查找時,有兩種方法:

    1. LHS ( lefthand side ):取得指定的目標
    2. 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
      
#Tokenizing #Parsing #Code-Generation #scope #LHS #RHS #ReferenceError #TypeError
「你所不知道的 JS 」系列書籍閱讀心得,未閱讀前對於 JavaScript 皆是懵懵懂懂,因面試時發現自己很多觀念都不正確不清楚,所以這次一探 JavaScript 的運作方式。 * 系列一開始會先把大方向簡短的整理,之後會以每個項目做詳細的筆記






Related Posts

每日心得筆記 2020-06-19(五)

每日心得筆記 2020-06-19(五)

Go 起手式之一

Go 起手式之一

Day 01: GraphQL and GitHub GraphQL API

Day 01: GraphQL and GitHub GraphQL API



Comments