iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 11
1
Modern Web

你懂 JavaScript 嗎?系列 第 11

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

  • 分享至 

  • xImage
  •  

你所不知道的 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

這裡有三個範疇...

  • (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 很邪惡,好孩子不要用!

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 引擎可考慮乾脆不要最佳化,因此程式碼就會跑得比較慢、效能比較差。

回顧

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

  • 語彙範疇是在語彙分析時期定義的範疇,而範疇的劃分在程式碼撰寫時就決定好了,之後任何企圖修改的行為都是不恰當的。
  • 範疇是編譯器或 JavaScript 引擎藉由識別字名稱查找變數的一組規則。其中,「遮蔽」是指只要找到巢狀範疇內第一個符合的識別字就會停止搜尋;「全域變數」必須使用 window.x 來避免被內層變數遮蔽;範疇的查找只適用於單層的識別字名稱,若為多層則是由物件存取規則來做解析。
  • eval 和 with 由於會修改語彙範疇,讓編譯時期所做的工都白費,因此效能不佳,應避免使用。

References


同步發表於部落格


上一篇
你懂 JavaScript 嗎?#10 範疇(Scope)
下一篇
你懂 JavaScript 嗎?#12 函式範疇與區塊範疇(Function vs Block Scope)
系列文
你懂 JavaScript 嗎?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言