你懂 JavaScript 嗎?#11 語彙範疇(Lexical Scope)
18 Oct 2018本文會提到
- 什麼是語彙範疇?這階段要做什麼事情?
- 什麼會改變語彙範疇?有什麼影響?
語彙範疇(Lexical Scope)
範疇的運作方式有兩種-語彙範疇(lexical scope)和動態範疇(dynamic scope),在這裡先來探討「語彙範疇」。
語彙分析階段會將字串解析成 token,例如:var a = 2;
會解析為 var
、a
、=
、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
這裡有三個範疇…
- (1) 最外面的範疇即全域範疇,識別字有 foo。
- (2) 中間的範疇是在 foo 裡面,識別字有 a、b、bar。
- (3) 最裡面的範疇是在 bar 裡面,識別字只有 c。
查找識別字
從上例可知,範疇的劃分說明了 JavaScript 引擎如何尋找識別字的所在之處。
這裡還要談兩個觀念「遮蔽(shadowing)」和「全域變數(global variable)」。
- 遮蔽(shadowing):若相同的識別字同時出現在不同的巢狀範疇中,那麼只要在巢狀範疇內層找到第一個符合的識別字就會停止搜尋。
- 全域變數(global variable):全域變數會自動變成全域物件的屬性,因此能使用
window.a
來避免 a 被巢狀範疇內層的同名變數遮蔽。
備註:範疇的查找只適用於一級識別字,例如: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 很邪惡,好孩子不要用!
…
…
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 已被禁止使用了。
…
…
為什麼 eval 和 with 會導致效能不佳?
JavaScript 引擎會在編譯時期進行最佳化,例如,靜態分析程式碼,確定變數和函式的宣告,這樣在執行時期就能節省解析識別字的成本。
但若在程式碼中有 eval 或 with,剛剛在編譯時期所確認的變數和函式的所在位置的結果都無效了,因為 JavaScript 引擎無法在編譯時期確認到底傳入什麼東西給 eval 或有什麼內容會讓 with 創建新的語彙範疇,所以也就不知道有什麼會改變語彙範疇了,也就是說,剛剛所做的最佳化都沒有意義了,JavaScript 引擎可考慮乾脆不要最佳化,因此程式碼就會跑得比較慢、效能比較差。
回顧
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…
- 語彙範疇是在語彙分析時期定義的範疇,而範疇的劃分在程式碼撰寫時就決定好了,之後任何企圖修改的行為都是不恰當的。
- 範疇是編譯器或 JavaScript 引擎藉由識別字名稱查找變數的一組規則。其中,「遮蔽」是指只要找到巢狀範疇內第一個符合的識別字就會停止搜尋;「全域變數」必須使用
window.x
來避免被內層變數遮蔽;範疇的查找只適用於單層的識別字名稱,若為多層則是由物件存取規則來做解析。 - eval 和 with 由於會修改語彙範疇,讓編譯時期所做的工都白費,因此效能不佳,應避免使用。
References
同步發表於2019 鐵人賽。