「詞法作用域(Lexical Scope)」是指編譯器在詞法分析/語意分析時所定義的範疇,除非在執行時期用特殊方法改動(如 JS 的 with
或 eval()
),否則在編譯完畢後不會再有所變化,因此也被稱為「靜態作用域/靜態範疇」。
前面說過,所謂的作用域其實是一組變數加上這些變數該如何查找的一串規範,規則大致如下:
因此在編譯的詞法分析階段,確定變數「如何宣告」(var
/ let
/ const
)以及「在何處宣告」(全域/函式/區塊作用域)後,就能斷明詞法作用域的查找規則。
詞法作用域僅處理第一層的物件識別字,如 a
、b
這樣一層的名稱。
像foo.bar.baz
這樣串鏈的參考,詞法作用域查詢僅執行 foo
的查找,bar
和 baz
的屬性解析則由物件屬性訪問規則(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
上面代碼中有三個作用域:
foo
a
、bar
、b
c
詞法作用域與變數調用的方式和位置無關,而是由宣告的方式和位置決定。
每個函式都創造了一個作用域,每個作用域都是互相獨立的,沒有任何交集的部分。
這些作用域氣泡的結構決定了程式在查找一個變數時需要查看的範圍與順序。
當 console.log(...)
被調用時,程式對 a
,b
和 c
進行取值查詢(RHS)。
首先從當前所在的作用域開始,並依序往外查找,當「找到第一個匹配的識別字後,查詢就會停止」,並回傳該識別字參考指向的值。
a
:
bar
作用域,找不到對應結果,往外一層查詢foo
作用域,找到變數 a
,回傳搜尋結果b
:
bar
作用域,找不到對應結果,往外一層查詢foo
作用域,找到變數 b
,回傳搜尋結果c
:
bar
作用域,找到變數 c
,回傳搜尋結果作用域按照這樣的規則依序向外查找,基本上有兩種結果:
ReferenceError
,表示可存取的作用域內找不到該變數,也就是值的參考(reference)。如果在同個作用域內重複宣告相同識別字,基本上有兩種結果。一是後面的宣告會覆蓋前面的(var
),或者回報 SyntaxError
結束編譯(let
/ const
)。
但同樣的識別字允許出現在不同作用域內,且互不干擾。在進行查找識別字時,程序會從內一路向外找尋,並在找到第一個符合結果時停下。
也就是說,當不同作用域擁有相同識別字時,位於內部的識別字會「遮蔽(shadowing)」外部的識別字,因為在遇見內部的識別字時,作用域查找就會停止。
全域變數會自動成為全域物件的屬性,如瀏覽器的
window
物件等。因此除了直接指向變數之外,也可以全域物件的屬性來呼叫變數,例如window.a;
,這種呼叫方式跳脫了作用域的遮蔽,讓全域變數能被訪問。
var
與 let
在之後的區塊作用域單元也會提到, 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
等等。
但在執行過程修改詞法作用域,就表示編譯時進行的優化很可能都變得無效,造成效能降低。因此如果無法完全確定改變詞法作用域後會發生什麼事,那最好永遠不要去改變它。
在了解詞法作用域後,下一篇接著來看看,什麼樣的方式能在全域作用域底下創造出獨立的作用域。