解析與編譯
Javascript 從源程序到可以被計算機識別的目標程序主要包含兩個階段:
- 解析生成抽象語法樹
- 編譯執行
解析
以V8引擎爲例,前置的解析被分爲兩種類型:Pre-Parser、Full-Parser。
Pre-Parser,主要負責對整個 Javascript 源代碼進行必要的前期檢查,判斷是否存在語法錯誤。只在 Top-level 代碼執行前進行。
這裏是一種比較普遍的流程,在使用某個事物之前,靠譜的做法當然是先明確下能不能用。
Full-Parser,做的工作相應比較多些,包含:
- 通過分詞/詞法分析、解析/語法分析生成抽象語法樹(Abstract Syntax Tree,AST)
-
進行作用域分析,爲變量分配內存,生成可用的上下文作用域。具體包括:
- 將形參作爲
GO/AO
的屬性,賦值爲實參值 - 將變量作爲
GO/AO
的屬性,賦值爲undefind
- 將函數作爲
GO/AO
的屬性,賦值爲其函數體 - 創建該函數的作用域鏈等
- 將形參作爲
GO
(Global Object),全局環境下創建全局對象。AO
(Active Object),函數執行前創建激活對象。
Full-Parser,在Top-level代碼和非Top-level代碼執行前都會進行。函數在被調用執行前,經過Full-Parser生成抽象語法樹提供給JIT編譯器,生成目標語言執行。
Top-level 是指源代碼初次加載時需要被首先運行到的“頂層”代碼。
V8引擎不一次性完成 Javascript 源代碼對應的 AST 信息,而是在知道要執行哪段代碼前,將這段代碼完成 AST 的生成。
想要了解 AST 信息,可查看 AST 生成工具。
問題:
【1】變量提升的原因是因爲爲了提高執行效率,在代碼執行前Full-Parser階段爲變量分配資源。
【2】函數聲明優先於變量聲明是因爲變量聲明只檢查變量是否存在,而函數聲明需要更新變量值。
編譯執行:
在瞭解JS的編譯過程前,先明確兩個概念:解釋器、編譯器。
解釋器就像口譯員,從源代碼第一行開始進行解析編譯執行。
編譯器則是直接將完整的源代碼完全編譯生成目標程序,從而快速執行。
解釋器與編譯器各有各的優勢,解釋器能夠快速啓動與執行,瀏覽器能夠快速執行JS代碼對Web頁面來說是非常重要的,這也是爲什麼瀏覽器使用解釋器來解析JS源代碼。
但是,在使用解釋器也存在着一些弊端,比如在處理循環的時候,解釋器並沒有很好的處理重複的“翻譯”工作。所以在早期(2008年以前)JS執行的速度並不是很快。
然而,編譯器除了編譯時間長一些,可以對代碼有更好的優化,從而能夠更快的執行代碼。因而,在2008年,多種瀏覽器添加了即時編譯器(JIT, just in time),使得JS的執行速度提高了10倍。
那麼JIT做了些什麼事情呢?
JIT包含兩部分構成:
- 基線編譯器
- 優化編譯器
首先源代碼會經過基線編譯器解析編譯生成未優化的目標代碼。同時JS引擎有稱爲監視器/分析器的部件,記錄代碼執行的次數和方式。
當某段代碼執行次數變多時,如函數頻繁調用、循環代碼塊等,基線編譯器會對這段代碼做一些優化。當這段代碼執行次數越來越多,監視器會將這段代碼交給優化編譯器,從而生成更快的版本。
爲了生成更快的版本,優化編譯器必須做一些假設,並且生成的代碼也是默認這些假設都成立的。但是,如果在代碼執行過程中,某個假設失敗了,瀏覽器將執行返回到解釋器或者基線編譯的版本。
這個過程稱爲去優化,所以循環中數據類型與結構的變化可能會對優化編譯過程造成影響。
具體流程如下: