1、編譯原理
在傳統編譯語言的流程中,程序中的一段代碼執行前會經歷三個步驟。統稱為“編譯”。
詞法分析
將代碼字符串分解成有意義的代碼塊,這些代碼塊稱為詞法單元。例如:在js中,var a = 2;。這段程序通常被拆分為以下詞法單元。var、a、2、;。至於空格是否會被當成詞法單元,取決於空格在這門語言中是否有意思。
語法分析
將詞法單元流(數組)轉換為“抽象語法樹”(AST,Abstract Syntax Tree。編譯原理課程中提到過)。
代碼生成
將AST轉換為可執行代碼。與語言,平台有關(java跨平台)。簡單來說:var a = 2;的AST被轉換成一組機器指令,用來創建一個a的變量(分配內存等),並將2存儲在a中。
而對於Javascript而言,盡管通常它被歸類為“動態”或“解釋執行”語言,但實際上它是一門編譯語言。所不同的是,在它編譯時引擎要執行更復雜的操作過程。
首先,Javascript引擎不會有大量的(向其他編譯器那麼多的)時間來進行優化,因為與其他語言不同,它的編譯過程不是在構建之前的。
對於Javascript而言,大部分編譯發生在代碼執行前的幾微秒(甚至更短)。所以引擎會用盡各種方法(比如JIT)來保證性能最佳。
簡單的說,任何Js代碼在執行前都要編譯(幾微秒前)。因此,在執行var a = 2;這段代碼前,引擎會先編譯,然後做好執行它的准備(加入到代碼隊列)。通常是馬上執行。
2、理解作用域
引擎
負責整個編譯以及執行過程。
編譯器
引擎的好朋友之一,負責語法分析和代碼生成等髒活累活。
作用域
引擎的另一個好朋友,負責收集和維護所有變量,並實施一套非常嚴格的規則,以保證當前代碼(作用域)對變量的訪問權限。
對於var a = 2;,它不僅僅是一句簡單的聲明。聲明它有兩個過程。編譯時:編譯器進行相關操作。執行時,Js引擎進行相關操作。
var a,編譯器會在當前作用域查找是否有a這個變量。如果有,則編譯器忽略此聲明。否則,在當前作用域創建一個a變量(分配內存)。
a = 2,接下來編譯器(語法分析,代碼生成…)生成運行時所需的代碼用來處理這個賦值操作。具體的賦值操作由Js引擎負責。Js引擎會在當前作用域查找a這個變量,如果找到,就進行賦值操作。否則,在父級作用域查找(作用域嵌套),直至全局作用域。如果找到,進行賦值操作。找不到拋出異常。
在查找作用域的過程中,會涉及到LHS查詢和RHS查詢。它們分別代表賦值操作的目標和賦值操作的源頭。不僅僅是賦值操作,更有函數賦值操作等等。比如:
function foo(a){ console.log(a); } foo(2);
最後一行foo()函數的調用需要對foo()本身進行RHS查詢。在全局作用域中找到了foo的聲明。並且()意味著要把foo當做一個函數執行,所以foo最好是一個函數,否則會報錯。
還有一個容易忽視的細節。在把2作為實參傳入到foo的形參時,會有一個隱式的a=2操作。a是賦值操作的源頭,2是賦值操作的目標。所以這裡對a進行了一次LHS查詢。由於在編譯過程中在當前作用域(函數作用域)將a聲明為foo的一個形參了,所以可以找到。
然後就是console.log(a);,console本身也需要一個LHS查詢,它是在window下面的內置對象,所以可以找到。然後對a進行RHS查詢。幸運的是,在將2賦值給函數形參a的時候,a已經聲明並賦值了。所以這個RHS是可以進行的。
3、作用域嵌套
在之前我們說過,作用域負責收集和維護所有變量,並實施一套非常嚴格的規則,以保證當前代碼(作用域)對變量的訪問權限。考慮以下代碼:
function foo(a){ console.log(a+b); } var b = 2; foo(2);
我們只考慮這裡對b的RHS引用。Js引擎開始試圖在foo函數作用域查找b變量,但是並沒有找到。於是,Js引擎就會突破當前限制,去外層作用域查找。哎呀,找到了!於是就對b進行RHS引用成功了。當然呢,要是沒找到的話,Js引擎也不會放棄,會繼續往外層作用域查找,直到找到全局作用域。然後遵循的規則參照a=2賦值那塊。
4、異常
在一個變量還沒有聲明(任何作用域都無法查到)的情況下,LHS和RHS查詢失敗後的操作是不一樣的。可以預料,RHS查詢失敗會拋出一個異常,那麼LHS查詢失敗呢?
function foo(a){ console.log(a+b); b = a; } foo(2);
第一次對b進行RHS查詢時,在任何作用域無法找到該變量的聲明。那麼有小伙伴就疑惑了,b=a呢?不是對b的聲明嗎?答案是:是。這裡確實是對b的聲明。
但在對作用域查找的過程中,只會向上查找聲明(涉及到聲明提升)。由於這裡b是在console.log()後面定義的。所以是失敗的,拋出ReferenceError異常。值得注意的是,ReferenceError是非常重要的異常類型。再考慮下述代碼:
function foo(a){ b = a; console.log(a+b); } foo(2);
這裡呢,第一次對b進行的是LHS查詢。如果在頂層(全局)作用域也無法查到foo的話,那麼Js引擎就會很熱心的幫你在全局作用域創建一個b變量,前提是在非“嚴格模式”下,在一個作用域內加上代碼“use strict”,表明使用嚴格模式。在嚴格模式下,LHS查詢失敗時,並不會創建一個全局變量,而是拋出同RHS查詢失敗時類似的ReferenceError異常。
接下來,加入你找到了這個變量,但是你試圖對這個變量進行不合理的操作。如:對一個非函數類型的變量進行()函數調用、對null或undefined類型的值進行訪問,那麼引擎會拋出另一種類型的異常,叫做TypeError。
總之,RefercenError同作用域判別失敗相關,而TypeError表示作用域判別成功,但是對結果的操作是不合法的。
5、小結
作用域是一套規則,規定在何處以及如何查找變量(加上之前說的,重要的事情說三遍)。如果查找的目的是賦值,就是進行LHS查詢。如果目的是獲取變量的值,就會進行RHS查詢。
Js引擎會在代碼執行前對其進行編譯。var a = 2;,這樣的操作會被分成兩個步驟。
1.編譯時, 編譯器聲明a變量,即var a。
2. 運行時,對a變量進行賦值。a=2。
LHS查詢和RHS查詢失敗會進行不同的操作。RHS查詢失敗會拋出ReferenceError異常。LHS查詢失敗會在全局作用域創建變量(非嚴格模式),在嚴格模式下拋出ReferenceError異常。
以上就是本文的全部內容,希望對大家有所幫助,希望大家繼續關注的最新內容。