看到Var a = 2你想到了什麼?

假若有人讓你解釋一下var a = 2這句代碼背後發生了什麼,若是你只想到了不就是一句聲明嗎?那你可能再去補一補JS比較底層的東西了。
其實這行代碼仍是涉及了比較多的知識點的。好比編譯原理,做用域,LHS和RHS查詢等一些知識了。下面就讓咱們一一揭開它們神祕的面紗。

編譯原理

咱們知道JavaScript是一門編譯語言,並且它不是提早編譯的。JS的編譯發生在代碼執行前的很是短的時間內。
程序中的源代碼在執行前會通過分詞、解析和代碼生成三個階段,咱們把這三個步驟統稱爲編譯react

  • 分詞/詞法分析
    這個階段既能夠稱之爲分詞,也能夠叫作詞法分析。以上二者的區別是很是微妙的,同時也是比較難懂的,我的感受不會區分這二者也無傷大雅。
    在這個過程由字符串組成的源代碼會被分解成有意義的代碼塊,這些代碼塊計算詞法單元。var a = 2就會被分解成var、a、=、2這些詞法單元。
  • 解析/語法分析 這個過程的任務就是將分詞階段生成的詞法單元流轉換成一個由元素逐級嵌套所組成的表明程序語法結構的樹。這個數就是大名鼎鼎的抽象語法樹(AST,Abstract Syntax Tree)。

這就是var a = 2這行代碼所生成的AST。

  • 代碼生成 咱們的計算機只能識別二進制,將AST轉換爲二進制可執行代碼的過程就是代碼生成。

做用域

在react中咱們常常會用到state來表示組件的狀態。狀態這個詞對於編程語言來講,我的感受是比較重要的,若沒有狀態這個詞,程序雖然也能執行一些簡單的任務,但會限制程序的靈活性。
在編程語言中,咱們使用變量來表示狀態。幾乎任何一種編程語言都有存儲、訪問和修改變量值的功能,正是這種存儲和訪問變量值得能力將狀態帶給了程序。
既然,有了變量,那麼咱們就須要考慮把變量存在哪裏以及如何在須要它們的時候可以找到它們?這時候咱們就須要設計一套良好的規則來存儲和訪問變量。做用域其實就是這套存儲和訪問變量的規則
做用域有三種:編程

  • 全局做用域
  • 函數做用域
  • 塊級做用域

在之後總結閉包的時候再來詳細解釋一下這三種做用域。畢竟,閉包和做用域和預編譯過程都是密不可分的。
爲了更好地理解做用域,咱們還要了解一下引擎和編譯器。bash

  • 引擎
    從頭至尾負責整個JS程序的編譯和執行過程。
  • 編譯器
    引擎的好朋友之一,負責語法分析及代碼生成等工做。

當咱們看到var a = 2是咱們就會把這句話當作一句聲明,但引擎會認爲這裏有兩個聲明,一個由編譯器在編譯階段處理,一個由引擎在運行時處理。
下面咱們將var a = 2進行分解。編譯器首先會將這行代碼分解成詞法單元,而後將這些詞法單元解析成一個樹結構。最後生成可執行的代碼。閉包

事實上,編譯器會作以下工做。編程語言

1.遇到var a,編譯器會詢問當前做用域內是否已經存在a變量,若是存在,則忽略該變量聲明,繼續往下編譯。若是沒有改變量,則在當前做用域內聲明一個名爲a的變量。
2.接下來編譯器會爲引擎生成運行時所需的代碼來進行a = 2的賦值操做。在執行階段,引擎會在當前做用域下查找a變量,若是找到a變量,引擎便會使用a變量,若是沒有找到,引擎會繼續查找該變量。函數

LHS和RHS(Left/Right Hand Search)

這兩種方法就是引擎查找變量的方法。
LHS和RHS的含義是「賦值操做的左側或右側」並不意味着就是「=賦值操做符的左側或右側」。賦值操做還有其餘幾種形式,所以在概念上最好將其理解爲「賦值操做的目標是誰(LHS)」以及「誰是賦值操做的源頭(RHS)」ui

  • LHS和RHS 都是出如今引擎對變量進行查詢的時候
  • LHS 變量賦值或寫入內存(將文本文件保存到硬盤中)
  • RHS 變量查找或從內容中讀取 想象爲從硬盤打開文本文件

特性:spa

  • 都會在全部做用域中查詢
  • 嚴格模式下,找不到所需的變量,引擎會ReferenceError異常
  • 非嚴格模式下,LHS會自動建立一個全局變量
  • 查詢成功後,若是對變量的進行不合理的操做會產生TypeError,例以下面代碼就會報錯
var a = 2;
a();
複製代碼

其實內部原理就是變量查詢。
下面經過一個例子練習一下。

function foo(a) {
    var b = a; 
    return a + b; 
}
var c = foo(2); 
複製代碼

LHS有3處:c = ..; a = 2(隱式變量分配),b = ..
RHS有4處:foo(2..、 = a; return 中的a和b設計

做用域鏈

當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以,在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到改變量或抵達最外層的做用域(全局做用域)爲止。
做用域鏈查找其實和預編譯是緊密相連的。3d

預編譯四步曲:

  1. 建立AO對象 (Activation Object, 執行期上下文,能夠理解爲做用域)
  2. 找形參和變量聲明,將變量和形參名做爲AO屬性名,值爲undefined
  3. 將實參值和形參統一
  4. 在函數體裏面找函數聲明,值賦予函數體
function fn(a) {
    console.log(a);
    var a = 123;
    console.log(a);
    function a() {}
    console.log(a);
    var b = function () {};
    console.log(b);
    function d() {}
}
fn(1);
複製代碼

1.建立AO
AO { }
2. 找形參和變量聲明,將變量和形參名做爲AO屬性名,值爲undefined
AO { a: undefined, b: undefined, }
3. 將實參值和形參統一
AO { a: 1, b: undefined, }
4. 在函數體裏面找函數聲明,值賦予函數體
AO { a: function a(){}, b: undefined,d: function d() {}}
預編譯完成後,引擎便要開始執行代碼了。

function fn(a) {
    console.log(a);//此時的AO中爲function a(){} 
    var a = 123;// var a 在預編譯時已經執行,不用再執行,此刻執行a = 123, AO中的a由函數變成a: 123
    console.log(a);// 123
    function a() {};// 預編譯已經執行,不用再執行
    console.log(a);// AO中a仍是123
    var b = function () {};// var b已經執行,執行 b = function(){},AO中b的值由undefined變成匿名函數
    console.log(b);// function() {}
    function d() {}:// 已經在編譯階段執行了,不在執行
}
fn(1);
複製代碼

因此以上代碼的結果爲function a(){},123,123,function(){}

再來練習一個

function a(age) {
    console.log(age);
    var age = 20;
    console.log(age);
    function age() {

    }
    console.log(age);
}
a(18);
複製代碼
  1. AO{}
  2. AO{age:undefined}
  3. AO{age:18}
  4. AO{age:function age(){}}

預編譯執行完成後,引擎開始執行代碼

function a(age) {
    console.log(age);// function age(){}
    var age = 20;// var age在預編譯是已經執行完,引擎執行age = 20, AO{age:function age(){}}變成AO{age:20}
    console.log(age);// 20
    function age() {

    }// 預編譯時已經執行完
    console.log(age);// AO中的age仍是20
}
a(18);
複製代碼

因此以上結果爲function age(){},20,20

若是咱們理解了預編譯和AO,再來理解閉包就會容易不少了。
相關文章
相關標籤/搜索