你不知道的JS系列【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編譯器編譯。設計

二、理解做用域

  咱們先了解JS編譯過程當中幾個名詞,JS引擎,編譯器,做用域。code

2.1.名詞介紹blog

  • 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)」。

三、做用域嵌套

  做用域是根據名稱查找變量的一套規則。實際狀況中,一般須要同時顧及幾個做用域。 當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以,在當前做用 域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量, 或抵達最外層的做用域(也就是全局做用域)爲止。

參考如下代碼:

var name='peer';
function sayHello(){
  alert('hello '+ name)
}
sayHello();
// 對name的RHS引用沒法在函數sayHello完成,可是能夠在上一級做用域中完成。

把做用域比喻成一個建築以下圖所示:

LHS和RHS引用都會在當前樓層進行查找,若是沒有找到,就會坐電梯前往上一層樓,若是仍是沒有找到就繼續向上,以此類推。一旦抵達頂層(全局做用域),可能找到了你所需的變量,也可能沒找到,但不管如何查找過程都將中止。

四、總結

  做用域是一套規則,用於肯定在何處以及如何查找變量(標識符)。LHS和RHS查詢都會在當前執行做用域中開始,若是有須要就會向上級做用域繼續查找目標標識符,這樣每次上升一級做用域,最後抵達全局做用域,不管找到或沒找到都將中止。不成功的RHS 引用會致使拋出ReferenceError異常。不成功的LHS引用會致使自動隱式地建立一個全局變量(非嚴格模式下),掌握這些基本做用域知識能使咱們更深刻理解JS引擎的編譯過程來編寫更高性能的代碼。


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

相關文章
相關標籤/搜索