前面的話
大多數時候,我們對作用域產生混亂的主要原因是分不清楚應該按照函式位置的巢狀順序,還是按照函式的呼叫順序進行變數查詢。再加上this機制的干擾,使得變數查詢極易出錯。這實際上是由兩種作用域工作模型導致的,作用域分為詞法作用域和動態作用域,分清這兩種作用域模型就能夠對變數查詢過程有清晰的認識。本文是深入理解javascript作用域系列第二篇——詞法作用域和動態作用域
詞法作用域
第一篇介紹過,編譯器的第一個工作階段叫作分詞,就是把由字元組成的字串分解成詞法單元。這個概念是理解詞法作用域的基礎
簡單地說,詞法作用域就是定義在詞法階段的作用域,是由寫程式碼時將變數和塊作用域寫在哪裡來決定的,因此當詞法分析器處理程式碼時會保持作用域不變
關係
無論函式在哪裡被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式被宣告時所處的位置決定
function foo(a) { var b = a * 2; function bar(c) { console.log( a, b, c ); } bar(b * 3); } foo( 2 ); // 2 4 12
在這個例子中有三個逐級巢狀的作用域。為了幫助理解,可以將它們想象成幾個逐級包含的氣泡
作用域氣泡由其對應的作用域塊程式碼寫在哪裡決定,它們是逐級包含的
氣泡1包含著整個全域性作用域,其中只有一個識別符號:foo
氣泡2包含著foo所建立的作用域,其中有三個識別符號:a、bar和b
氣泡3包含著bar所建立的作用域,其中只有一個識別符號:c
查詢
作用域氣泡的結構和互相之間的位置關係給引擎提供了足夠的位置資訊,引擎用這些資訊來查詢識別符號的位置
在程式碼片段中,引擎執行console.log(...)宣告,並查詢a、b和c三個變數的引用。它首先從最內部的作用域,也就是bar(...)函式的作用域開始查詢。引擎無法在這裡找到a,因此會去上一級到所巢狀的foo(...)的作用域中繼續查詢。在這裡找到了a,因此引擎使用了這個引用。對b來講也一樣。而對c來說,引擎在bar(...)中找到了它
[注意]詞法作用域查詢只會查詢一級識別符號,如果程式碼引用了foo.bar.baz,詞法作用域查詢只會試圖查詢foo識別符號,找到這個變數後,物件屬性訪問規則分別接管對bar和baz屬性的訪問
foo = { bar:{ baz: 1 } }; console.log(foo.bar.baz);//1
遮蔽
作用域查詢從執行時所處的最內部作用域開始,逐級向外或者說向上進行,直到遇見第一個匹配的識別符號為止
在多層的巢狀作用域中可以定義同名的識別符號,這叫作“遮蔽效應”,內部的識別符號“遮蔽”了外部的識別符號
var a = 0; function test(){ var a = 1; console.log(a);//1 } test();
全域性變數會自動為全域性物件的屬性,因此可以不直接通過全域性物件的詞法名稱,而是間接地通過對全域性物件屬性的引用來對其進行訪問
var a = 0; function test(){ var a = 1; console.log(window.a);//0 } test();
通過這種技術可以訪問那些被同名變數所遮蔽的全域性變數。但非全域性的變數如果被遮蔽了,無論如何都無法被訪問到
動態作用域
javascript使用的是詞法作用域,它最重要的特徵是它的定義過程發生在程式碼的書寫階段
那為什麼要介紹動態作用域呢?實際上動態作用域是javascript另一個重要機制this的表親。作用域混亂多數是因為詞法作用域和this機制相混淆,傻傻分不清楚
動態作用域並不關心函式和作用域是如何宣告以及在任何處宣告的,只關心它們從何處呼叫。換句話說,作用域鏈是基於呼叫棧的,而不是程式碼中的作用域巢狀
var a = 2; function foo() { console.log( a ); } function bar() { var a = 3; foo(); } bar();
【1】如果處於詞法作用域,也就是現在的javascript環境。變數a首先在foo()函式中查詢,沒有找到。於是順著作用域鏈到全域性作用域中查詢,找到並賦值為2。所以控制檯輸出2
【2】如果處於動態作用域,同樣地,變數a首先在foo()中查詢,沒有找到。這裡會順著呼叫棧在呼叫foo()函式的地方,也就是bar()函式中查詢,找到並賦值為3。所以控制檯輸出3
兩種作用域的區別,簡而言之,詞法作用域是在定義時確定的,而動態作用域是在執行時確定的