讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議166:掌握JavaScript預編譯過程 >

建議166:掌握JavaScript預編譯過程

JavaScript解析過程可以分為編譯和執行兩個階段。編譯就是常說的JavaScript預處理(即預編譯)。在預編譯期,JavaScript解釋器將完成對JavaScript代碼的預處理,也就是說,把JavaScript腳本代碼轉換成字節碼。在執行期,JavaScript解釋器借助執行期環境將字節碼生成機械碼,並按順序執行,完成程序設計的任務。

JavaScript是一種解釋型語言,而不是編譯型語言。所謂解釋型語言,就是代碼在執行時才被解釋器逐行動態編譯和執行,而不是在執行之前就完成編譯。而編譯型語言是先編譯後執行,兩者的操作過程不同。當程序被編譯時,需要一個被稱為編譯器的程序來完成所有工作。一般編譯器可以包括下面一些組件(如圖9.2所示)。

❑符號表:其中存儲所有的符號及其信息,如類型、範圍等。

❑詞法分析器:其功能是將字符流(即腳本字符串)轉換為記號(如關鍵詞、操作符等)。

❑語法分析器:其功能是讀取記號流,並建立語法樹。

❑語義檢查器:用來檢查語法樹的語義錯誤。

❑中間代碼生成器:用來把語法樹轉換為中間代碼。

❑代碼優化器:用來優化中間代碼。

❑代碼生成器:用來將中間代碼生成二進制字節碼。

圖 9.2 編譯器的構成和工作流程示意圖

編譯程序的一般步驟分為:詞法分析、語法分析、語義檢查、代碼優化和生成字節碼。但是,對於JavaScript這類解釋型語言來說,通過詞法分析和語法分析,並建立語法樹之後,就開始解釋執行了,而不是在完全生成字節碼之後,再調用虛擬機來執行這些編譯好的字節碼。在詞法分析過程中,JavaScript解釋器先把腳本代碼的字符流轉換為記號流。例如,把字符流:


a=(b-c);


轉換為記號流:


NAME\"a\"

EQUALS

OPEN_PARENTHESIS

NAME\"b\"

MINUS

NAME\"c\"

CLOSE_PARENTHESIS

SEMICOLON


詞法分析器是編譯器中與源程序直接接觸的部分,因此,詞法分析器可以實現:

❑去掉註釋,自動生成文檔。

❑提供錯誤位置(可以通過記錄行號來提供),當字符流變成詞法記號流以後,就沒有了行的概念。

❑完成預處理,如C語言中的宏定義等。

詞法結構是JavaScript語言基礎,至於詞法分析的實現就比較複雜,這裡就不再深入研究,讀者只需要簡單瞭解它的工作機制即可。

詞法分析和語法分析不是完全獨立的,而是交錯進行的,也就是說,詞法分析器不會在讀取所有的詞法記號後再使用語法分析器來處理。在通常情況下,每取得一個詞法記號,就將其送入語法分析器進行分析(如圖9.3所示)。

圖 9.3 詞法分析和語法分析示意圖

詞法分析是對JavaScript腳本代碼進行逐一分析的過程,相當於語言翻譯。例如,把英文逐詞逐句地譯成中文,英文就是源代碼,而中文就是代碼的記號。

語法分析的過程就是把詞法分析所產生的記號生成語法樹,通俗地說,就是把從程序中收集的信息存儲到數據結構中。注意,在編譯中用到的數據結構有兩種:符號表和語法樹。

❑符號表:就是在程序中用來存儲所有符號的一個表,包括所有的字符串變量、直接量字符串,以及函數和類。

❑語法樹:就是程序結構的一個樹形表示,用來生成中間代碼。

下面是一個簡單的條件結構和輸出信息代碼段,被語法分析器轉換為語法樹之後,如圖9.4所示。


if(typeof a==\"undefined\"){

a=0;

}

else{

a=a;

}

alert(a);


圖 9.4 語法樹結構示意圖

如果JavaScript解釋器在構造語法樹的時候發現無法構造,就會報語法錯誤,並結束整個代碼塊的解析。對於傳統強類型語言來說,在通過語法分析構造出語法樹後,翻譯出來的句子可能還會有模糊不清的地方,需要進一步的語義檢查。語義檢查的主要部分是類型檢查。例如,函數的實參和形參類型是否匹配。但是,對於弱類型語言來說,就沒有這一步。

經過編譯階段的準備,JavaScript代碼在內存中已經被構建為語法樹,然後JavaScript引擎就會根據這個語法樹結構邊解釋邊執行。

在解釋過程中,JavaScript引擎是嚴格按照作用域機制來執行的。JavaScript語法採用的是詞法作用域,也就是說,JavaScript的變量和函數作用域是在定義時決定的而不是在執行時決定的。由於詞法作用域取決於源代碼結構,因此JavaScript解釋器只需要通過靜態分析就能確定每個變量、函數的作用域,這種作用域也稱為靜態作用域。

當JavaScript解釋器執行每個函數時,先創建一個執行環境,在這個虛擬環境中創建一個調用對象,在這個對象內存儲當前域中所有局部變量、參數、嵌套函數、外部引用和父級引用列表upvalue等語法分析結構。

實際上,通過聲明語句定義的變量和函數,在預編譯期的語法分析中就已經存儲到符號表中了,只要把它們與調用對像中的同名屬性進行映射即可。調用對象的生命週期與函數的生命週期是一致的,在函數調用完畢且沒有外部引用的情況下,調用對像會自動被JavaScript引擎當做垃圾進行回收。

另外,JavaScript解釋器通過作用域鏈把多個嵌套的作用域連在一起,並借助這個鏈條幫助JavaScript解釋器檢索變量的值。這個作用域鏈相當於一個索引表,通過編號來存儲作用域的嵌套關係。JavaScript解釋器在檢索變量的值時會按著這個索引編號進行快速查找,直到找到全局對像為止,如果沒有找到,則傳遞一個特殊的undefined值。

如果函數引用了外部變量的值,則JavaScript解釋器會為該函數創建一個閉包體。閉包體是一個完全封閉和獨立的作用域,它不會在函數調用完畢後就被JavaScript引擎當做垃圾進行回收。閉包體可以長期存在,因此開發人員常把閉包體當做內存中的蓄水池,專門用於長期保存變量的值。

只有當閉包體的外部引用被全部設置為null值時,該閉包才會被回收。當然,也容易引發「垃圾氾濫」,甚至出現內存外溢現象。