1. 程式人生 > >JavaScript 作用域不完全指北

JavaScript 作用域不完全指北

什麼是作用域

對於幾乎所有程式語言,最基本的功能之一就是能夠儲存變數的值,並且能在之後對這個值進行訪問和修改。這樣就會帶來幾個問題,這些變數儲存在哪裡?程式在需要的時候又是如何找到它們的?要解決這些問題,就需要引入一套規則來儲存變數和訪問變數,這套規則就是作用域。

理解作用域

在剛開始接觸 JavaScript 這門語言時,肯定會經常接觸到 JavaScript 是動態語言, 是解釋執行的,但事實上 JavaScript 是一門編譯語言。只不過和 Java、C# 這些傳統意義上的編譯語言不同,JavaScript 的編譯過程不是發生在構建之前的。大部分情況下,JavaScript 的編譯過程發生在程式碼執行前的很短時間內。也就是說 JavaScript 程式碼在執行前都要進行編譯。

為了更好地理解作用域,我們需要明確下面幾個概念

  • 引擎

從頭到尾負責整個 JavaScript 程式的編譯及執行過程

  • 編譯器

負責語法分析及程式碼生成等髒活累活

  • 作用域

負責收集並維護由所有宣告的識別符號(變數) 組成的一系列查詢, 並實施一套非常嚴格的規則, 確定當前執行的程式碼對這些識別符號的訪問許可權。

下面我們從引擎、編譯器和作用域的角度,分析 var a = 2 這條宣告語句,看看它們是如何協同完成工作的

  1. 遇到 var a, 編譯器會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域的集合中。 如果是, 編譯器會忽略該宣告, 繼續進行編譯; 否則它會要求作用域在當前作用域的集合中宣告一個新的變數, 並命名為a。
  2. 接下來編譯器會為引擎生成執行時所需的程式碼, 這些程式碼被用來處理 a = 2 這個賦值操作。 引擎執行時會首先詢問作用域, 在當前的作用域集合中是否存在一個叫作 a 的變數。 如果是, 引擎就會使用這個變數; 如果否, 引擎會繼續查詢該變數。如果引擎最終找到了 a 變數, 就會將 2 賦值給它。 否則引擎就會舉手示意並丟擲一個異常!

簡單來說,變數的賦值操作會執行兩個動作, 首先編譯器會在當前作用域中宣告一個變數(如果之前沒有宣告過), 然後在執行時引擎會在作用域中查詢該變數, 如果能夠找到就會對它賦值,否則就會並丟擲一個異常。

作用域巢狀

我們知道引擎查詢變數的過程在作用域中進行的,而這個過程通常會涉及多個作用域。

當一個塊或函式巢狀在另一個塊或函式中時, 就發生了作用域的巢狀。 因此, 在當前作用域中無法找到某個變數時, 引擎就會在外層巢狀的作用域中繼續查詢, 直到找到該變數,或抵達最外層的作用域(也就是全域性作用域) 為止。

為了便於理解,可以將作用域巢狀比喻成一棟高樓,我們從一樓(當前作用域)開始查詢,如果沒有找到,就會前往上一個樓層繼續查詢,以此類推。一旦到達頂層(全域性作用域),可能找到,也可能沒有找到,查詢過程都必須停止。

LHS查詢和RHS查詢

繼續上文的示例,引擎在執行編譯器生成的程式碼時,會通過查詢變數 a 來判斷它是否已經宣告過。查詢的過程由作用域進行協助, 但是引擎執行怎樣的查詢, 會影響最終的查詢結果。查詢過程分為兩類:LHS查詢和RHS查詢。其實很簡單,當變量出現在賦值操作的左側時進行 LHS 查詢, 出現在右側時進行 RHS 查詢。
更準確一點的講, RHS 查詢是查詢某個變數的值,而 LHS 查詢是查詢變數的容器本身,從而可以對其賦值。如下面的示例:

var a = 2; // a: LHS查詢
var b = 3; // b: LHS查詢
a = b; //a: LHS查詢 b:RHS查詢

為什麼區分 LHS 和 RHS 是一件重要的事情?

因為在變數還沒有宣告(在任何作用域中都無法找到該變數) 的情況下,  LHS 和 RHS兩種查詢的行為是不一樣的。

1.當引擎執行 RHS 查詢時,如果 RHS 查詢在所有巢狀的作用域中遍尋不到所需的變數, 引擎就會丟擲 ReferenceError異常。 

console.log(a); //ReferenceError: a is not defined

2.當引擎執行 LHS 查詢時, 如果在頂層(全域性作用域) 中也無法找到目標變數,全域性作用域中就會建立一個具有該名稱的變數, 並將其返還給引擎, 前提是程式執行在非“嚴格模式” 下。如果在“嚴格模式”下,引擎也會丟擲 ReferenceError異常。

//非嚴格模式
var a =2;
b = a;
console.log(b); //2
//嚴格模式
'use strict';
var a =2;
b = a;
console.log(b); //ReferenceError: b is not defined

另外,如果 RHS 查詢找到了一個變數, 但是你嘗試對這個變數的值進行不合理的操作,比如試圖對一個非函式型別的值進行函式呼叫, 或著引用 null 或 undefined 型別的值中的屬性, 那麼引擎會丟擲另外一種型別的異常, 叫作TypeError。

ReferenceError 代表作用域判別失敗相關, 而 TypeError 則代表作用域判別成功了, 但是對結果的操作是非法或不合理的。

參考

《你不知道的JavaScript》

作者:CoderFocus

微信公眾號:

宣告:本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。如果您認為還不錯,不妨點選一下下方的【推薦】按鈕,謝謝支援。轉載與引用請註明作者及出處。