Javascript作用域和作用域鏈原理解析
作用域和作用域鏈在Javascript和很多其它的程式語言中都是一種基礎概念。但很多Javascript開發者並不真正理解它們,但這些概念對掌握Javascript至關重要。
正確的去理解這個概念有利於你去寫更好,更高效和更簡潔的程式碼,讓你成為一個更優秀的Javascript開發者。
因此,在本文中,我將會向大家解釋清楚什麼是作用域和作用域鏈,以及Javascript引擎在內部是如何通過它們操作和查詢變數的。
事不宜遲,正文開始 :)
什麼是作用域
Javascript中的作用域說的是變數的可訪問性和可見性。也就是說整個程式中哪些部分可以訪問這個變數,或者說這個變數都在哪些地方可見。
為什麼作用域很重要
作用域最為重要的一點是安全。變數只能在特定的區域內才能被訪問,有了作用域我們就可以避免在程式其它位置意外對某個變數做出修改。
作用域也會減輕命名的壓力。我們可以在不同的作用域下面定義相同的變數名。
作用域的型別
Javascript中有三種作用域:
- 全域性作用域;
- 函式作用域;
- 塊級作用域;
1. 全域性作用域
任何不在函式中或是大括號中宣告的變數,都是在全域性作用域下,全域性作用域下宣告的變數可以在程式的任意位置訪問。例如:
// 全域性變數 var greeting = 'Hello World!'; function greet() { console.log(greeting); } // 列印 'Hello World!' greet();
2. 函式作用域
函式作用域也叫區域性作用域,如果一個變數是在函式內部宣告的它就在一個函式作用域下面。這些變數只能在函式內部訪問,不能在函式以外去訪問。例如:
function greet() { var greeting = 'Hello World!'; console.log(greeting); } // 列印 'Hello World!' greet(); // 報錯: Uncaught ReferenceError: greeting is not defined console.log(greeting);
3. 塊級作用域
ES6引入了let和const關鍵字,和var關鍵字不同,在大括號中使用let和const宣告的變數存在於塊級作用域中。在大括號之外不能訪問這些變數。看例子:
{ // 塊級作用域中的變數 let greeting = 'Hello World!'; var lang = 'English'; console.log(greeting); // Prints 'Hello World!' } // 變數 'English' console.log(lang); // 報錯:Uncaught ReferenceError: greeting is not defined console.log(greeting);
上面程式碼中可以看出,在大括號內使用var宣告的變數lang是可以在大括號之外訪問的。使用var宣告的變數不存在塊級作用域中。
作用域巢狀
像Javascript中函式可以在一個函式內部宣告另一個函式一樣,作用域也可以巢狀在另一個作用域中。請看例子:
var name = 'Peter'; function greet() { var greeting = 'Hello'; { let lang = 'English'; console.log(`${lang}: ${greeting} ${name}`); } } greet();
這裡我們有三層作用域巢狀,首先第一層是一個塊級作用域(let宣告的),被巢狀在一個函式作用域(greet函式)中,最外層作用域是全域性作用域。
詞法作用域
詞法作用域(也叫靜態作用域)從字面意義上看是說作用域在詞法化階段(通常是編譯階段)確定而非執行階段確定的。看例子:
let number = 42; function printNumber() { console.log(number); } function log() { let number = 54; printNumber(); } // Prints 42 log();
上面程式碼可以看出無論printNumber()在哪裡呼叫console.log(number)都會列印42。動態作用域不同,console.log(number)這行程式碼列印什麼取決於函式printNumber()在哪裡呼叫。
如果是動態作用域,上面console.log(number)這行程式碼就會列印54。
使用詞法作用域,我們可以僅僅看原始碼就可以確定一個變數的作用範圍,但如果是動態作用域,程式碼執行之前我們沒法確定變數的作用範圍。
像C,C++,Java,Javascript等大多數程式語言都支援靜態作用域。Perl 既支援動態作用域也支援靜態作用域。
作用域鏈
當在Javascript中使用一個變數的時候,首先Javascript引擎會嘗試在當前作用域下去尋找該變數,如果沒找到,再到它的上層作用域尋找,以此類推直到找到該變數或是已經到了全域性作用域。
如果在全域性作用域裡仍然找不到該變數,它就會在全域性範圍內隱式宣告該變數(非嚴格模式下)或是直接報錯。
例如:
let foo = 'foo'; function bar() { let baz = 'baz'; // 列印 'baz' console.log(baz); // 列印 'foo' console.log(foo); number = 42; console.log(number); // 列印 42 } bar();
當函式bar()被呼叫,Javascript引擎首先在當前作用域下尋找變數baz,然後尋找foo變數但發現在當前作用域下找不到,然後繼續在外部作用域尋找找到了它(這裡是在全域性作用域找到的)。
然後將42賦值給變數number。Javascript引擎會在當前作用域以及外部作用域下一步步尋找number變數(沒找到)。
如果是在非嚴格模式下,引擎會建立一個number的全域性變數並把42賦值給它。但如果是嚴格模式下就會報錯了。
結論:當使用一個變數的時候,Javascript引擎會循著作用域鏈一層一層往上找該變數,直到找到該變數為止。
作用域和作用域鏈是如何工作的
以上內容已經講解了作用域,作用域的型別,現在讓我們看下Javascript引擎是如何確定變數的作用域鏈和如何去查詢變數的。
要想理解Javascript是如何進行變數查詢的,必須要了解Javascript中詞法環境這個概念(請參考:理解Javascript中的執行上下文和執行棧)。
什麼是詞法環境
所謂詞法環境就是一種識別符號—變數對映的結構(這裡的識別符號指的是變數/函式的名字,變數是對實際物件[包含函式和陣列型別的物件]或基礎資料型別的引用)。
簡單地說,詞法環境是Javascript引擎用來儲存變數和物件引用的地方。
注意:不要混淆了詞法環境和詞法作用域,詞法作用域是在程式碼編譯階段確定的作用域(譯者注:一個抽象的概念),而詞法環境是Javascript引擎用來儲存變數和物件引用的地方(譯者注:一個具象的概念)。
一個詞法環境就像下面這樣:
lexicalEnvironment = { a: 25,obj: <ref. to the object> }
只有當該作用域的程式碼被執行的時候,引擎才會為那個作用域建立一個新的詞法環境。詞法環境還會記錄所引用的外部詞法環境(即外部作用域)。例:
lexicalEnvironment = { a: 25,obj: <ref. to the object> outer: <outer lexical environemt> }
Javascript引擎是如何進行變數查詢的
現在我們已經知道了作用域,作用域鏈和詞法環境的概念,現在讓我們看下Javascript引擎是如何利用詞法環境來確定作用域和作用域鏈的。
結合例子我們來理解上面的這些概念:
let greeting = 'Hello'; function greet() { let name = 'Peter'; console.log(`${greeting} ${name}`); // Hello Peter } greet(); { let greeting = 'Hello World!' console.log(greeting); // Hello World! }
上述程式碼載入後,首先會建立一個全域性詞法環境,其中包含在全域性範圍內宣告的變數和函式。像下面這樣:
globalLexicalEnvironment = { greeting: 'Hello' greet: <ref. to greet function> outer: <null> }
這裡的outer欄位(也就是外部詞法環境)被設定為了null,是因為全域性詞法環境已經是最頂層的詞法環境了。
然後,我們呼叫了greet()函式,然後一個新的詞法環境會被被建立:
functionLexicalEnvironment = { name: 'Peter' outer: <globalLexicalEnvironment> }
這裡的outer欄位被設定為了globalLexicalEnvironment,是因為他的外部作用域就是全域性作用域。
然後,執行console.log(`${greeting} ${name}`)這行程式碼,Javascript引擎首先在當前函式的詞法環境中尋找變數greeting和name,但只找到了name,沒找到greeting。然後繼續在上層的詞法環境中找greeting(這裡是全域性作詞法環境)。最後在全域性詞法環境中找到了greeting。
緊接著執行那段在大括號裡的程式碼,為這個塊級建立一個新的詞法環境。如下:
blockLexicalEnvironment = { greeting: 'Hello World',outer: <globalLexicalEnvironment> }
然後執行console.log(greeting)這行程式碼,首先在本層詞法環境中找greeting,OK,找到,結束。此時就不會再去外部作用域(這裡是全域性作用域)尋找該變量了。
注意:只有let和const宣告變數才會建立一個新的詞法環境儲存,使用var宣告的變數會被儲存在當前塊(大括號)所在的詞法環境中(全域性詞法環境或是函式詞法環境中)。
結論:當一個變數被使用時,Javascript引擎會首先在當前的詞法環境中進行尋找,如果找不到就找上層詞法環境中尋找,直到找到為止。
結論
作用域就是一個變數可訪問和可見的區域,和函式一樣,Javascript的作用域也可以巢狀,Javascript引擎會層層遍歷作用域來尋找用到的變數。
Javascript使用詞法作用域,這意味著變數的作用在編譯階段就會被確定。
Javascript引擎在程式執行期間使用詞法環境來儲存變數和函式。
作用域和作用域鏈是Javascript中的基礎概念,理解作用域和作用域鏈能讓你成為一個更優秀的Javascript開發者。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。