iT邦幫忙

2022 iThome 鐵人賽

DAY 8
0
Modern Web

就是要搞懂 JavaScript 啦!系列 第 8

Day08 宣告領土──詞法作用域

  • 分享至 

  • xImage
  •  

「詞法作用域(Lexical Scope)」是指編譯器在詞法分析/語意分析時所定義的範疇,除非在執行時期用特殊方法改動(如 JS 的 witheval()),否則在編譯完畢後不會再有所變化,因此也被稱為「靜態作用域/靜態範疇」。

前面說過,所謂的作用域其實是一組變數加上這些變數該如何查找的一串規範,規則大致如下:

  • 宣告方式決定變數本身的存取規則
  • 作用域能夠彼此嵌套,並且只有內部作用域可以存取外部作用域的變數。

因此在編譯的詞法分析階段,確定變數「如何宣告」(var / let / const)以及「在何處宣告」(全域/函式/區塊作用域)後,就能斷明詞法作用域的查找規則。

第一層物件識別字

詞法作用域僅處理第一層的物件識別字,如 ab 這樣一層的名稱。
foo.bar.baz 這樣串鏈的參考,詞法作用域查詢僅執行 foo 的查找,barbaz 的屬性解析則由物件屬性訪問規則(object property-access rules)負責。


查詢

// 全域作用域
function foo(a) {  // foo 的作用域開始

	var b = a * 2;

	function bar(c) {  // bar 的作用域開始
		console.log( a, b, c );
	}

	bar(b * 3);
}

foo( 2 ); // 2 4 12

作用域氣泡說明
圖片來源

上面代碼中有三個作用域:

  1. 全域:含有識別字 foo
  2. 包圍著 foo 的作用域:含有識別字 abarb
  3. 包圍著 bar 的作用域:含有識別字 c

詞法作用域與變數調用的方式和位置無關,而是由宣告的方式和位置決定。

每個函式都創造了一個作用域,每個作用域都是互相獨立的,沒有任何交集的部分。

這些作用域氣泡的結構決定了程式在查找一個變數時需要查看的範圍與順序。

console.log(...) 被調用時,程式對 ab 和 c 進行取值查詢(RHS)。

首先從當前所在的作用域開始,並依序往外查找,當「找到第一個匹配的識別字後,查詢就會停止」,並回傳該識別字參考指向的值。

  • a
    1. 搜尋 bar 作用域,找不到對應結果,往外一層查詢
    2. 搜尋 foo 作用域,找到變數 a,回傳搜尋結果
  • b
    1. 搜尋 bar 作用域,找不到對應結果,往外一層查詢
    2. 搜尋 foo 作用域,找到變數 b,回傳搜尋結果
  • c
    1. 搜尋 bar 作用域,找到變數 c,回傳搜尋結果

作用域按照這樣的規則依序向外查找,基本上有兩種結果:

  • 找到符合的識別字,並停止查詢
  • 抵達最外層(全域作用域)後依然沒有符合結果,於是發生 ReferenceError,表示可存取的作用域內找不到該變數,也就是值的參考(reference)。

遮蔽 shadowing

如果在同個作用域內重複宣告相同識別字,基本上有兩種結果。一是後面的宣告會覆蓋前面的(var),或者回報 SyntaxError 結束編譯(let / const)。

但同樣的識別字允許出現在不同作用域內,且互不干擾。在進行查找識別字時,程序會從內一路向外找尋,並在找到第一個符合結果時停下。

也就是說,當不同作用域擁有相同識別字時,位於內部的識別字會「遮蔽(shadowing)」外部的識別字,因為在遇見內部的識別字時,作用域查找就會停止。

全域變數會自動成為全域物件的屬性,如瀏覽器的 window 物件等。因此除了直接指向變數之外,也可以全域物件的屬性來呼叫變數,例如 window.a;,這種呼叫方式跳脫了作用域的遮蔽,讓全域變數能被訪問。

varlet

在之後的區塊作用域單元也會提到, let 能夠創造屬於區塊的變數,而 var 無法。

所以 let 能夠遮蔽 var,它在內層創造了一個獨立的作用域:

{
  var special = "Java";
  {
    let special = "JavaScript"; 
    console.log(special); // JavaScript
  }
}

var 無法遮蔽 let ,兩個 special 屬於同一個作用域:

{
  let special = "Java";
  {
    var special = "JavaScript";
    console.log(special);
    // SyntaxError,不得重複宣告變數 special 
  }
}

性能優化

由於 JS 屬於詞法/靜態作用域,在編譯的詞法分析階段能夠進行靜態分析,提前決定好所有變數和宣告的位置,減少在執行期間對變數的解析和查詢。

作弊手段

JS 當中有一些能夠修改作用域的方法,比如 eval() 和已被廢棄的 with 等等。

但在執行過程修改詞法作用域,就表示編譯時進行的優化很可能都變得無效,造成效能降低。因此如果無法完全確定改變詞法作用域後會發生什麼事,那最好永遠不要去改變它。

在了解詞法作用域後,下一篇接著來看看,什麼樣的方式能在全域作用域底下創造出獨立的作用域。


參考資料


上一篇
Day07 變數存在的泡泡空間──作用域
下一篇
Day09 這是我的地盤──函式作用域
系列文
就是要搞懂 JavaScript 啦!73
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言