JS學習系列 01 - 編譯原理和做用域

在學習 javascript 的過程當中,咱們第一步最應該瞭解和掌握的就是做用域,與之相關還有程序是怎麼編譯的,變量是怎麼查找的,js 引擎是什麼,引擎和做用域的關係又是什麼,這些是 javascript 這門語言最基礎的地基,至於對象、函數、閉包、原型鏈、做用域鏈以及設計模式等等都是地基以上的建築,只有地基打牢了,建築纔會穩。一樣只有先把最基礎的部分掌握了,以後的擴展學習纔會更容易。javascript

這一節我要說的,就是做用域和編譯原理,從這裏開始,我會一點點的把深刻學習 javascript 的過程當中總結的知識點以及遇到的問題,一篇一篇的梳理出來,若是有志同道合的朋友,能夠關注我這個系列,咱們一塊兒玩轉 javascript。java

1. 編譯原理

你們一般把 javascript 歸類爲一種「動態」或「解釋執行」的語言,但事實上,它是一門編譯語言,但和傳統的編譯語言不一樣,它不是提早編譯的,編譯結果也不能進行移植。設計模式

在傳統編譯語言中,程序在執行以前會經歷三個步驟,統稱爲「編譯」:數組

  • 分詞/詞法分析

這個過程會把字符串分解成有意義的代碼塊,這些代碼塊被稱爲詞法單元
例如 var a = 5; 這段程序一般會被分解成下面這些詞法單元: var、a、=、五、; 。空格是否會被當成詞法單元取決於空格在這門語言中是否有意義。微信

  • 解析/語法分析

這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法結構的樹。這個樹被稱爲「抽象語法樹」(Abstract Syntax Tree,AST)。
var a = 5; 的抽象語法樹中可能以下圖所示:
抽象語法樹閉包

  • 代碼生成

將 AST 轉換爲可執行代碼的過程被稱爲代碼生成。這個過程與語言、目標平臺等息息相關。簡單來講,就是經過某種方法能夠將 var a = 5; 的 AST 轉化爲一組機器指令,用來建立一個叫作 a 的變量(包括分配內存等),並將一個值 5 存儲在 a 中。函數

比起那些編譯過程只有三個步驟的語言的編譯器來講,javascript 引擎要複雜的多
例如,在詞法分析和代碼生成階段有特定的步驟來對運行性能進行優化,包括對冗餘元素進行優化等。性能

首先咱們要清楚,javaScript 引擎不會有太多的時間來進行優化(相對於其它語言的編譯器來講),由於與其它語言不一樣,javascript 的編譯過程不是發生在構建以前的學習

對於 javascript 來講,大部分狀況下編譯發生在代碼執行前的幾微秒(甚至更短)的時間內。在咱們將要討論的做用域背後,javascript 引擎用盡了各類辦法(好比 JIT,能夠延遲編譯甚至從新編譯)來保證性能最佳。優化

總結來講,任何 javascript 代碼片斷在執行前都要進行編譯(預編譯)。所以,javascript 編譯器首先會對 var a = 5; 這段程序進行編譯,而後作好執行它的準備,而且一般立刻就會執行它。

2. 三位好友

要真正理解做用域,咱們首先要知道 javascript 中有三位好朋友:

  • 引擎

從頭至尾負責整個 javascript 程序的編譯及執行過程。

  • 編譯器

負責語法分析及代碼生成。

  • 做用域

負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。

當碰見 var a = 5; 這一段代碼時,其實執行了兩個步驟:

(1)var a; 編譯器會詢問做用域是否已經有一個該名稱的變量存在於同一做用域的集合中。若是是,編譯器會忽略該聲明,繼續進行編譯,不然它會要求在當前做用域的集合中聲明一個新的變量,並命名爲 a 。
(2)a = 5; 編譯器會爲引擎生成運行時所需的代碼,這些代碼用來處理 a = 5; 這個賦值操做。引擎運行時會首先詢問做用域,在當前做用域的集合中是否存在一個叫做 a 的變量,若是是,引擎就會使用這個變量。若是否,引擎會繼續向父級做用域中查找,直到找到全局做用域,若是在全局做用域中仍沒有找到 a ,那麼在非嚴格模式下,引擎會爲全局對象新建一個屬性 a ,並將其賦值爲5,在嚴格模式下,引擎會報錯誤 ReferenceError: a is not defined

總結來講,變量的賦值會執行兩個操做,首先編譯器會在當前做用域聲明一個變量(若是以前沒有聲明過),而後在運行時引擎會在當前做用域中查找該變量(找不到就向上一級做用域查找),若是可以找到就會對它賦值。

3. LHS 和 RHS

前面說到引擎在爲變量賦值的時候會在做用域中查找變量,可是執行怎樣的查找,用什麼方式,會對最終的查找結果形成影響。

var a = 5; 這個例子中,引擎會對 a 進行 LHS 查詢,固然,另一個查找類型叫做 RHS。

對變量進行賦值所執行的查詢叫 LHS。
找到並使用變量值所執行的查詢叫 RHS。

舉個例子:

function foo(a) {
   // 這裏隱式包含了 a = 2 這個賦值,因此對 a 進行了 LHS 查詢
   var b = a;
   // 這裏對 a 進行了 RHS 查詢,找到 a 的值,而後對 b 進行 LHS 查詢,把 2 賦值給 b
   return a + b; 
   // 這裏包含了對 a 和 b 進行的 RHS 查詢
}

var c = foo(2);
// 這裏首先對 foo 進行 RHS 查詢,找到它是一個函數,而後對 c 進行 LHS 查詢把 foo 賦值給 c

因此上面的例子共包含 3 個 LHS 查詢和 4 個 RHS 查詢,大家都找對了嗎?

4. 做用域嵌套

當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域嵌套。所以,在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量,或抵達最外層的做用域(也就是全局做用域)爲止。

舉個例子:

function foo(a) {
   console.log(a + b);
}

var b = 2;

foo(2);    // 4

這裏對 b 進行的 RHS 查詢在 foo 做用域中沒法找到,但能夠在上一級做用域(這個例子中就是全局做用域)中找到。

總結來講,遍歷嵌套做用域鏈的規則很簡單:引擎從當前執行的做用域中開始查找變量,若是都找不到,就向上一級繼續查找。當抵達最外層的全局做用域時,不管找到仍是沒找到,查找過程都會中止。

5. 總結

編譯器、引擎和做用域是 javascript 代碼執行的基礎,掌握好這些會對咱們深刻學習 javascript 起到事半功倍的效果,咱們的學習之路纔剛剛開始,你們加油!

歡迎關注個人公衆號

微信公衆號

參考文章

  1. 《你不知道的JavaScript》
相關文章
相關標籤/搜索