iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 10
2

你所不知道的 JS

本文會提到

  • 什麼是「範疇」?範疇的功用是?
  • 編譯器怎麼理解程式碼?
  • 什麼是巢狀範疇?
  • 從 LHS 與 RHS 來理解 JavaScript 查找變數的報錯機制。

範疇(Scope)

範疇(Scope)是指編譯器或 JavaScript 引擎藉由識別字名稱(identifier name)查找變數的一組規則。

找東西

編譯器怎麼理解程式碼?

編譯器會在程式執行前將程式碼由上到下逐行轉為電腦可懂的命令,稍後會執行這個編譯後的結果。注意,JavaScript 引擎會在每次執行前即時編譯程式碼(約幾毫秒 (ms) 而已),接著立刻執行編譯後的指令。

編譯有三步驟

  • 語法基本單元化與語彙分析(tokenizing/lexing):將字串解析成 token,例如:var a = 2; 就會解析 vara=2;
  • 剖析或稱語法分析(parsing):承上,將這些 token(vara=2;)組成抽象語法樹(abstract syntax tree,AST)

抽象語法樹(abstract syntax tree,AST)

  • 產生目的程式碼(code-generation):承上,將 AST 轉為可執行的程式碼,通常是機器語言,在這裡也會做最佳化。

範疇的功用是?

團隊合作

在編譯的過程中,JavaScript 引擎、編譯器和範疇會互相溝通以完成工作。它們各自負責的任務有

  • JavaScript 引擎:負責整個編譯過程並執行程式碼。
  • 編譯器:負責編譯三步驟-語法基本單元化與語彙分析、剖析或稱語法分析、產生目的程式碼。
  • 範疇:負責維護變數清單。

範例如下,這裡有一段程式碼,在編譯這段程式碼時,JavaScript 引擎、編譯器和範疇三者會做什麼呢?

var a = 2;
  1. 在編譯的時候,編譯器會先到範疇詢問變數 a 是否存在,若不存在就宣告這個 a 變數。
  2. 接著,在執行階段,JavaScript 引擎先到範疇詢問變數 a 是否存在,若存在就將 2 設定給它;若 a 不存在就報錯。

其中,引擎對範疇的查找變數的動作,可分為兩種類型

  • LHS(left-hand side):要查找的變數在指定動作的左邊,例如:a = 2 的 a 在等號的左邊,就是執行 LHS 查找動作。
  • RHS(right-hand side):變數不在指定動作的左邊,例如:console.log(a),就是執行 RHS 查找動作。

備註:函式宣告(例如:function foo() {...})並不是 LHS!這是因為在做函式宣告時,就同時做了宣告和值的定義,而非在執行階段設定其值。

巢狀範疇(Nested Scope)

若在目前執行的範疇找不到這個變數的時候,就會往外層的範疇搜尋,持續搜尋直到找到為止,或直到最外層的全域範疇(global scope);而這樣一層又一層的範疇就稱為「巢狀範疇」(nested scope)。

鴨子轉圈圈

如下,console.log(a + b) 中,b 的 RHS 無法在 foo 中解析完成,但可到全域範疇解析出來。

const foo = (a) => {
  console.log(a + b);
}

const b = 2;

foo(2); // 4

錯誤(Error)

為什麼需要理解 LHS 和 RHS 呢?這是因為要看懂 JavaScript 報錯的原因。

錯誤(Error)

當解析 identifier 失敗時

  • 若是 RHS,則會丟出 ReferrenceError 的訊息。
  • 若是 LHS,就會分為是否在嚴格模式(strict mode)的情況
    • 在非嚴格模式下,會在全域建立這個變數。
    • 在嚴格模式下,會丟出 ReferrenceError 的訊息。

還有一種狀況,不論在 LHS 和 RHS 下,操作不合法的行為時,就會丟出 TypeError 的訊息。

  • LHS:重新設定已宣告為 const 變數,const a = 2; a = 4;a = 4 會導致 TypeError。
  • RHS:執行不是 function 的變數,const b = 2; b();b() 會導致 TypeError。

範例

這裡有一個小範例,判斷哪裡發生了 LHS?哪裡發生了 RHS?

const foo = (a) => {
  const b = a;
  return a + b;
}

const c = foo(2);

答案是...

...

...

...

LHS

  1. const c = ...
  2. const b = ...
  3. 隱含的參數設定 a = 2

RHS

  1. const b = a 其中的 ... = a 對 a 取值
  2. return a + b 其中要對 a 取值
  3. return a + b 其中要對 b 取值
  4. foo(a) 其中要對 foo 取得其函數

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...

  • 範疇是指編譯器或 JavaScript 引擎藉由識別字名稱查找變數的一組規則。
  • 編譯器會在程式執行前將程式碼由上到下逐行轉為電腦可懂的命令,而稍後執行的即是這個編譯後的結果。編譯有三步驟:語法基本單元化與語彙分析、剖析或稱語法分析、產生目的程式碼。並且,在編譯的過程中,JavaScript 引擎、編譯器和範疇會互相溝通以完成工作。
  • 在查找變數的過程中,若在目前執行的範疇找不到這個變數的時候,就會往外層的範疇搜尋,持續搜尋直到找到為止,或直到最外層的全域範疇。
  • 從 LHS 與 RHS 來理解 JavaScript 查找變數的報錯機制。

References


同步發表於部落格


上一篇
你懂 JavaScript 嗎?#9 文法(Grammar)
下一篇
你懂 JavaScript 嗎?#11 語彙範疇(Lexical Scope)
系列文
你懂 JavaScript 嗎?30

尚未有邦友留言

立即登入留言