你懂 JavaScript 嗎?#11 語彙範疇(Lexical Scope)

你所不知道的 JS

本文會提到

語彙範疇(Lexical Scope)

範疇的運作方式有兩種-語彙範疇(lexical scope)和動態範疇(dynamic scope),在這裡先來探討「語彙範疇」。

語彙分析階段會將字串解析成 token,例如:var a = 2; 會解析為 vara=2;。語彙範疇是在語彙分析時期所定義的範疇,而範疇的劃分在程式碼撰寫時就決定好了,之後任何企圖修改的行為都是不恰當的。

參考以下程式碼,試著區分有幾個範疇?誰是誰的巢狀範疇

function foo(a) {
  var b = a * 2;

  function bar(c) {
    console.log(a, b, c);
  }

  bar(b * 3);
}

foo( 2 ); // 2 4 12

答案是…

範疇的劃分

圖片來源:You Don’t Know JS: Scope & Closures, Chapter 2: Lexical Scope

這裡有三個範疇…

查找識別字

從上例可知,範疇的劃分說明了 JavaScript 引擎如何尋找識別字的所在之處。

這裡還要談兩個觀念「遮蔽(shadowing)」和「全域變數(global variable)」。

備註:範疇的查找只適用於一級識別字,例如:a、b 這樣單層的名稱。如果是要找 foo.bar.a 的話,範疇的查找只會找到 foo,之後的 bar 和 a 就會由物件存取規則(object property-access rules)來繼續解析。

什麼會改變語彙範疇?有什麼影響?

有兩個方法會在執行時修改語彙範疇-eval 和 with。

eval

範例如下,在 foo 內執行 eval,導致 console.log(...) 時 JavaScript 引擎尋找 b 時在 foo 這個範疇找到(其值為 3),而遮蔽了全域的 b(其值為 2)。

function foo(str, a) {
  eval(str);
  console.log(a, b);
}

var b = 2;

foo('var b = 3;', 1); // 1 3

eval 很邪惡,好孩子不要用!

Don't do it!

with

with 會在執行時期創建新的語彙範疇,這裡來看一個全域值外漏的例子。

當 with 區塊執行時,with 將物件參考當成範疇來看,這個物件的特性就會成為該範疇內的識別字。因此,a = 2 其實是在做 LHS 的動作,若在 o2 和 foo 的範疇找不到 a,就會往全域範疇來找,由於在此並非嚴格模式,因此在找不到的情況下,就會生出一個全域變數 a 並設定其值為 2。

function foo(obj) {
  with (obj) {
    a = 2;
  }
}

var o1 = {
  a: 3,
};

var o2 = {
  b: 3,
};

foo(o1);
console.log(o1.a); // 2

foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2,全域值外漏

幸好,with 已被禁止使用了。

ban!

為什麼 eval 和 with 會導致效能不佳?

JavaScript 引擎會在編譯時期進行最佳化,例如,靜態分析程式碼,確定變數和函式的宣告,這樣在執行時期就能節省解析識別字的成本。

但若在程式碼中有 eval 或 with,剛剛在編譯時期所確認的變數和函式的所在位置的結果都無效了,因為 JavaScript 引擎無法在編譯時期確認到底傳入什麼東西給 eval 或有什麼內容會讓 with 創建新的語彙範疇,所以也就不知道有什麼會改變語彙範疇了,也就是說,剛剛所做的最佳化都沒有意義了,JavaScript 引擎可考慮乾脆不要最佳化,因此程式碼就會跑得比較慢、效能比較差。

回顧

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

References


同步發表於2019 鐵人賽


You-Dont-Know-JS javascript 你所不知道的JS 2019鐵人賽 你懂JavaScript嗎? 鐵人賽 You-Dont-Know-JS-Scope-and-Closures 你懂 JavaScript 嗎?2019 iT 邦幫忙鐵人賽 系列文