1. 程式人生 > >深入學習Java Scipt之作用域和閉包

深入學習Java Scipt之作用域和閉包

引擎與作用域及編譯器

在傳統的編譯語言的流程中,程式的一段原始碼主要分成三步,統稱為“編譯”

  •   分詞/詞法分析

    它的主要作用是將字元組成的字串分解成有意義的程式碼塊,例如:var a=2;者會被分解成“var”,“a”,“=”,“2”。

空格是否被分解主要是看空格在該程式語言中有沒有意義。

  •  解析/語法分析

  這個過程是將詞法單元流(陣列)轉換成由元素逐級巢狀所組成的程式語法結構的樹。這棵樹被稱為“抽象語法樹”

  • 程式碼生成

  將AST轉換成可執行程式碼的過程。如果不考慮具體細節的話,那麼簡單來說此過程便是將var a=2這一組AST轉換成機器指令,用來建立一個叫做a的變數(分配記憶體),將2儲存在a中。

  Java Script引擎要比這個過程複雜得多,但是它沒有那麼多時間來進行優化或者對程式碼的其他處理,因為Java Script程式碼通常在程式碼執行前的幾微秒內進行編譯和處理,看到這,也許你會有疑惑,為什麼Java Script引擎能這麼快呢?這是因為在我們討論的作用域背後Java Script用盡了各種辦法,將效能達到最優化。

作用域

  要想了解作用域是什麼?起到怎麼樣的作用,必須還得了解以下的概念。

  • 引擎

  從頭到尾負責程式碼的編譯與執行

  • 編譯器

  負責語法分析與程式碼生成

  • 作用域

  負責收集和維護所有宣告的識別符號組成的一系列查詢,並實施一套非常嚴格的規則,判斷當前執行的程式碼對變數訪問的許可權。通俗點來講就像是倉庫的管理員,它負責物品(宣告的識別符號)的安全(維護),以及的上架(收集)。對來訪者(引擎或者編譯器)取物品進行檢查判斷(判斷許可權)。

若你還是覺得很難理解,那沒有關係,我們舉個例子
 **例Java Script執行程式碼“ var a=2”。當引擎見到此宣告時,它不僅會自己處理,還會將此宣告拋給編譯器進行處理。**

  **編譯器的處理**:將“var a=2”分解為詞法單元,由這些詞法單元生成抽象語法樹。然後接著生成程式碼,那麼由虛擬碼描述“var a=2”,它的執行結果是這樣的:“為一個變數分配記憶體,並命名為a,然後將2這個值儲存在變數裡面”。儘管看上去很完美,但是由於作用域的存在,編譯器不得不進行另一種操作。

   遇到“var a=2”,編譯器會先向作用域詢問是否有一個該名稱的變數存在於作用域中,是的話它就會忽略這次宣告,繼續編譯。否的話,它就會要求作用域在當前作用域集合中建立一個名為"a"的變數,然後再編譯。我們來按照之前的倉庫理解,編譯器是對商品(程式碼)加工的工人,當它遇到“var a=2”的物品清單(宣告),它會將這個物品羅列出來(詞法分析),然後看看物品之間的需求需要(抽象語法樹),接著去倉庫找物品(生成程式碼),在倉庫的門口遇到了倉庫管理員(當前的作用域集合),提出了物品要求,倉庫管理員進庫檢索物品(檢視是否建立了名為"a"的變數),若有,那麼將物品遞給工人(忽略此次宣告);沒有的話,則向總部提出此庫("當前的作用域結合")物品進貨(建立"a"變數)
   接下來編譯器將為引擎運生成執行的程式碼,這些程式碼用來處理a=2這個賦值操作。引擎執行時會首先詢問作用域,當前作用域中是否存在"a"這個變數,是的話,繼續操作,不是的話,將在作用域中急促查詢。
引擎最終找到了"a"這個變數,便會將2賦值給它,不然的話,它就會丟擲一個異常。


  總結:在對"var a=2"的執行過程中,編譯器首先對程式碼進行詞法分析、語法分析,然後建立變數(如果作用域中不存在的話),執行時,引擎再一次在作用域中查詢變數,如果找到的話就給它賦值。(經歷了兩次變數查詢,此處的作用域指的是當前的作用域集合)

 

引擎查詢變數的方式


  在編譯器將宣告生成可執行程式碼時,我們的引擎將會進行變數查詢,引擎進行怎樣的查詢將會影響到查詢的結果。
   在Java Script中引擎查詢變數涉及到的查詢方式主要有兩種,LHS查詢以及RHS查詢,**LHS是在賦值操作左側查詢**,**RHS是賦值操作右側查詢**,在我們的例子中,“var a=2”涉及到的是LHS查詢。
   例:
      console.log(a);
      在這裡面就涉及到了RHS查詢,怎麼理解呢?在這程式碼裡面"a"沒有值,相應的它需要查詢得到"a"的值,這樣的話才能把值傳給console.log();

我們把兩個例子進行比較

      a=2

  在這裡面我們並不需要查詢"a"的值,而是要為"=2"這一個操作找到一個目標。

為了更加熟悉"LHS"以及"RHS",我們再舉個例子

-------------------------------------------

  function foo(a){

console.log(a);

};

foo(2);

---------------------------------------------

  首先我們先看看foo()函式,console.log(a);中涉及到了"RHS"查詢。

在foo(2)中我們也涉及到了"RHS"查詢(查詢foo()),但是,別忽略了,在此函式中將"2"的值傳給形參"a"即"a=2"此處涉及到了"LHS"查詢。

  這裡面涉及到了幾次"RHS"以及"LHS"查詢呢?

foo(2)------一次"RHS'查詢,查詢foo()函式,

console.log(a)--------三次"RHS"查詢,一次查詢console,一次.log,一次"a"

在傳遞形參時-------一次"LHS"查詢

總結:"LHS"是找到操作的容器,"RHS"是查詢變數的值

那麼為什麼要區分LHS和RHS,這對我們除錯Java Script是有好處的

  • 當作用域進行RHS查詢時,查詢不到結果時,它便會丟擲ReferncError錯誤,而在查詢到變數,但是對變數的引用不正確的情況下,會丟擲TypeError錯誤。
  • 當作用域進行LHS查詢時,在非ES5的嚴格模式下,它查詢不到變數時,便會主動建立一個變數。而在ES5的嚴格模式下,它則會彈出ReferenceError錯誤。

作用域巢狀

  在我們舉得例子中,如編譯器對變數的查詢,引擎對變數的查詢都是在當前作用域集合中操作的,通常不僅只有這麼一個作用域,而是多個。

  例如,當一個塊或者函式在另一個塊或者函式中時,便會發生作用域的巢狀,首先它們會在當前作用域下查詢變數,沒有查詢到則返回外層作用域(直到最外層全域性作用域)--------------這就是作用域的巢狀。

舉個例子:

        function foo(a){

    console.log(a+b);

}        

var b=2;

foo(2);

在foo函式中我們需要對b進行RHS引用(查詢b),但是在foo()函式這一層的作用域中,並沒有查詢到b,於是它便回到外層作用域查詢b,在"var b=2"中查詢到了b。

運用我們之前講過的倉庫例子,引擎是一名採購商人,它需要b這一個商品(RHS引用),於是去問了當前商店所在的倉庫管理員(當前作用域),“這裡有b這個商品嗎?”,“沒有,但是您可以去更高級別的倉庫(外層作用域)找找”倉庫管理員回答,於是採購商便去更高級別的倉庫找到了b。

巢狀規則:從當前的作用域開始查詢,向更高級別的作用域查詢,直到最高級別的作用域,查詢不到則停止丟擲異常(根據RHS以及LHS不同丟擲的異常不同)

 

異常

  為什麼要知道LHS與RHS查詢呢??這對我們除錯Java Script程式碼是有好處的。

例如:   function(a){

console.log(a+b);

b=a;

}

foo(b);

  我們在foo()中對b進行RHS查詢,但是在當前的作用域中查詢不到b,根據作用域巢狀規則,我們返回最外一層,發現也沒有b。此時我們丟擲ReferenceError異常-------即RHS查詢不到相關變數則丟擲ReferenceError異常。

  而LHS則不同,如果在最外層的作用域中查詢不到變數的話,它就會自作主張的建立一個變數,此時不會丟擲異常

  但是在ES5的情況下,LHS表現則不同,在ES5(嚴格模式)下禁止自動或者隱式建立變數,這就意味著進行LHS查詢不能夠自動建立變量了!!!-------此時丟擲ReferenceError異常

   RHS查詢還有一個細節,就是當你查詢到變數,但對變數的操作不正確時,也會丟擲異常,例如在對一個數組執行大於陣列下標的操作時--------丟擲TypeError異常。

總結:

  • RHS查詢不到變數時丟擲ReferenceError異常,查詢到變數對變數操作不正確時丟擲Type異常

  • LHS在非ES5模式下,查詢不到變數時,自動建立變數。在ES5模式下,查詢不到變數時,丟擲Reference異常

 

本章小結

  作用域實際上是一套用於確定變數的位置以及查詢變數的規則。程式碼的編譯分為三部分,詞法分析--語法分析--程式碼生成。前兩步是編譯器在執行,直到生成可執行程式碼時,引擎才開始從作用域中查詢變數。

  像Var a=2可分為兩步

  1、宣告變數:var a將會在當前的作用域內建立a變數,分配記憶體

  2、賦值操作:a=2會查詢(LHS)變數a,然後進行賦值操作

  RHS查詢是找到變數的值

  LHS查詢是為變數進行賦值操作(包括實參形參的傳遞)

  RHS以及LHS查詢都從當前作用域開始,一層一層往更高階作用域查詢,直到全域性作用域停止(找到變數立即停止)。

  當RHS沒查詢到變數時,丟擲ReferenceError異常,查詢到變數但變數操作不正確時,發生TypeError異常。

  不在嚴格模式(ES5)下,當LHS沒查詢到變數時,它自動隱式建立變數。在嚴格模式(ES5)下,LHS不允許自動隱式建立變數,丟擲ReferenceError異常。