1. 程式人生 > >你不知道的JS系列【1】- 什麼是作用域

你不知道的JS系列【1】- 什麼是作用域

  幾乎所有的程式語言都能夠儲存變數,並且能在之後對這個變數值進行訪問或修改,正是儲存和訪問變數的能力將狀態帶給了程式,那麼,這些變數儲存在哪裡呢?程式需要時又是如何找到他們?這些問題說明需要一套設計良好的規則來儲存變數,並且之後可以方便的找到這些變數,這套規則被稱為作用域。

1、瞭解編譯原理

  儘管將JS歸類為“動態”或“解釋執行”指令碼語言,但事實上它是一門編譯語言。但是與傳統編譯語言不同的是,它不是提前編譯的,編譯結果也不能在分散式系統中進行移植。JS引擎進行編譯的步驟與傳統的語言非常相似,程式中一段原始碼在執行之前會經歷三個步驟,統稱為“編譯”。

  • 分詞/詞法分析

這個過程會將由字元組成的字串分解成有意義的程式碼塊,這些程式碼塊被稱為詞法單元。例如,考慮程式 var a = 2;

。這段程式通常會被分解成 為下面這些詞法單元:var、a、=、2 、;

  • 解析/語法分析

這個過程是將詞法單元流(陣列)轉換成一個由元素逐級巢狀所組成的代表了程式語法結構的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。var a = 2;的抽象語法樹中可能會有一個叫作VariableDeclaration的頂級節點,接下來是一個叫作 Identifier(它的值是 a)的子節點,以及一個叫作 AssignmentExpression 的子節點。AssignmentExpression 節點有一個叫作 NumericLiteral(它的值是 2)的子節點。

  • 程式碼生成

AST轉換為可執行程式碼的過程稱被稱為程式碼生成,簡單來說就是有某種方法可以將 var a = 2;的AST轉化為一組機器指 令,用來建立一個叫作a的變數(包括分配記憶體等),並將一個值儲存在a中。

編譯流程如下圖所示:

JS引擎比傳統的編譯語言編譯器複雜很多,在語法分析和程式碼生成階段有特定的步驟來對效能進行優化,大部分情況下編譯發生在程式碼之前的前幾微秒,在討論作用域背後,js引擎用了各種辦法來保證效能最佳。

Tips:我們平時在寫JS程式碼的時候,一個語句結尾要加分號(;),便於JS編譯器編譯。

2、理解作用域

  我們先了解JS編譯過程中幾個名詞,JS引擎,編譯器,作用域。

2.1.名詞介紹

  • JS引擎:從頭到尾負責整個JS程式編譯過程。
  • 編譯器:負責語法分析及程式碼生成等。
  • JS引擎:負責收集並維護由所有宣告的識別符號(變數)組成的一系列查 詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權。

2.2.變數賦值

對於var a=2;這段程式碼,我們認為這就是申明一個為變數a且初始值為2,實際上,JS引擎認為這裡有兩個完全不同的申明,一個由編譯器在編譯時處理,另一個則由引擎在執行時處理。

處理過程分為兩步:

1.遇到var a,編譯器會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域的集合中。如果是,編譯器會忽略該宣告,繼續進行編譯;否則它會要求作用域在當前作用域的集合中宣告一個新的變數,並命名為a。

2.接下來編譯器會為引擎生成執行時所需的程式碼,這些程式碼被用來處理a = 2這個賦值操作。引擎執行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫作a的變數。如果是,引擎就會使用這個變數;如果否,引擎會繼續查詢該變數。

如果引擎最終找到a變數,就會將2賦值給它。否則就丟擲異常。

Tips:宣告提前(hoist)-JS引擎在建立變數時,會將該變數提升到當前作用域的最前面。

總結:變數的賦值操作會執行兩個動作,首先編譯器會在當前作用域中宣告一個變數(如果之前沒有宣告過),然後在執行時引擎會在作用域中查詢該變數,如果能夠找到就會對它賦值。

2.3.LHS查詢&RHS查詢

編譯器在編譯過程中的第二步生成了程式碼,引擎在執行時,會通過查詢變數a來判斷它是否已經宣告過。當變量出現在賦值操作的左側時進行LHS查詢,當變量出現在右側時進行RHS查詢。

console.log(a); //對a的引用時RHS引用,這裡沒有對a賦予任何值,需要查詢a的值。

a=2; //對a的引用是LHS引用,因為這裡不關心a的值等於多少,只想為 =2 這個賦值操作找到一個目標(變數a);

LHS和RHS的含義是“賦值操作的左側或右側”並不一定意味著就是“= 賦值操作符的左側或右側”。賦值操作還有其他幾種形式,因此在概念上最好將其理解為“賦值操作的目標是誰(LHS)”以及“去找到XX變數的值,誰是賦值操作的源頭(RHS)”。

3、作用域巢狀

  作用域是根據名稱查詢變數的一套規則。實際情況中,通常需要同時顧及幾個作用域。 當一個塊或函式巢狀在另一個塊或函式中時,就發生了作用域的巢狀。因此,在當前作用 域中無法找到某個變數時,引擎就會在外層巢狀的作用域中繼續查詢,直到找到該變數, 或抵達最外層的作用域(也就是全域性作用域)為止。

參考以下程式碼:

var name='peer';
function sayHello(){
  alert('hello '+ name)
}
sayHello();
// 對name的RHS引用無法在函式sayHello完成,但是可以在上一級作用域中完成。

把作用域比喻成一個建築如下圖所示:

LHS和RHS引用都會在當前樓層進行查詢,如果沒有找到,就會坐電梯前往上一層樓,如果還是沒有找到就繼續向上,以此類推。一旦抵達頂層(全域性作用域),可能找到了你所需的變數,也可能沒找到,但無論如何查詢過程都將停止。

4、總結

  作用域是一套規則,用於確定在何處以及如何查詢變數(識別符號)。LHS和RHS查詢都會在當前執行作用域中開始,如果有需要就會向上級作用域繼續查詢目標識別符號,這樣每次上升一級作用域,最後抵達全域性作用域,無論找到或沒找到都將停止。不成功的RHS 引用會導致丟擲ReferenceError異常。不成功的LHS引用會導致自動隱式地建立一個全域性變數(非嚴格模式下),掌握這些基本作用域知識能使我們更深入理解JS引擎的編譯過程來編寫更高效能的程式碼。


參考資料:
《你不知道的JavaScript