基本概念
編譯器驯遇,解釋器
抽象語法樹
字節(jié)碼和機(jī)器碼
編譯器和解釋器
計(jì)算機(jī)不能直接理解高級(jí)語言怨规,只能直接理解機(jī)器語言羡疗,所以必須要把高級(jí)語言翻譯成機(jī)器語言挺物,計(jì)算機(jī)才能執(zhí)行高級(jí)語言編寫的程序。根據(jù)語言的執(zhí)行流程纺棺,可以把語言分成編譯型語言和解釋型語言榄笙。
編譯型語言:程序在執(zhí)行之前需要一個(gè)專門的編譯過程,把程序編譯成 為機(jī)器語言的文件祷蝌,運(yùn)行時(shí)不需要重新翻譯办斑,直接使用編譯的結(jié)果就行了。程序執(zhí)行效率高,依賴編譯器乡翅,跨平臺(tái)性差些。如C罪郊、C++蠕蚜、go等.
解釋型語言: 程序不需要編譯,程序在運(yùn)行時(shí)才翻譯成機(jī)器語言(所以執(zhí)行前需要環(huán)境中安裝了解釋器)悔橄,每執(zhí)行一次都要翻譯一次靶累。因此效率比較低。效率比較低癣疟,依賴解釋器挣柬,跨平臺(tái)性好。
編譯型與解釋型睛挚,兩者各有利弊邪蛔, 不能一概而論。前者由于程序執(zhí)行速度快扎狱,同等條件下對(duì)系統(tǒng)要求較低侧到,因此像開發(fā)操作系統(tǒng)、大型應(yīng)用程序淤击、數(shù)據(jù)庫系統(tǒng)等時(shí)都采用它匠抗,像C/C++、Pascal/Object Pascal(Delphi)等都是編譯語言污抬,而一些網(wǎng)頁腳本汞贸、服務(wù)器腳本及輔助開發(fā)接口這樣的對(duì)速度要求不高、對(duì)不同系統(tǒng)平臺(tái)間的兼容性有一定要求的程序則通常使用解釋性語言印机,如JavaScript矢腻、VBScript、Perl耳贬、Python踏堡、Ruby、MATLAB 等等咒劲。
我們都知道 JavaScript 存在變量提升顷蟆,在函數(shù)作用域內(nèi)的任何變量的聲明都會(huì)被提升到頂部并且值為 undefined。
所以JS引擎好像對(duì)同一個(gè)腳本執(zhí)行了兩次腐魂,第一次完成所有聲明帐偎,然后第二次才執(zhí)行代碼?還是先編譯整個(gè)代碼然后運(yùn)行它蛔屹?這兩種都不對(duì)削樊。
其實(shí)變量聲明不過只執(zhí)行上下文的小把戲。在執(zhí)行任何語句之前,解釋器就要從創(chuàng)建執(zhí)行上下文后已經(jīng)存在的作用域中找到變量的值漫贞。
抽象語法樹
抽象語法樹(Abstract Syntax Tree甸箱,AST),或簡(jiǎn)稱語法樹(Syntax tree)迅脐,是源代碼語法結(jié)構(gòu)的一種抽象表示芍殖。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)谴蔑。之所以說語法是“抽象”的豌骏,是因?yàn)檫@里的語法并不會(huì)表示出真實(shí)語法中出現(xiàn)的每個(gè)細(xì)節(jié)。比如隐锭,嵌套括號(hào)被隱含在樹的結(jié)構(gòu)中窃躲,并沒有以節(jié)點(diǎn)的形式呈現(xiàn);而類似于 if-condition-then 這樣的條件跳轉(zhuǎn)語句钦睡,可以使用帶有兩個(gè)分支的節(jié)點(diǎn)來表示蒂窒。
字節(jié)碼和機(jī)器碼
字節(jié)碼(Byte-code):是一種包含執(zhí)行程序、由一序列 op 代碼/數(shù)據(jù)對(duì)組成的二進(jìn)制文件赎婚。字節(jié)碼是一種中間碼刘绣,它比機(jī)器碼更抽象。
機(jī)器碼 (Machine-code):計(jì)算機(jī)直接使用的程序語言挣输,其語句就是機(jī)器指令碼纬凤,機(jī)器指令碼是用于指揮計(jì)算機(jī)應(yīng)做的操作和操作數(shù)地址的一組二進(jìn)制數(shù)。
JavaScript代碼執(zhí)行過程
生成AST(抽象語法樹)
生成字節(jié)碼
執(zhí)行代碼
生成AST
生成AST的步驟可以拆分成以下兩個(gè)小步驟:
- 詞法分析:將JavaScript代碼解析成一個(gè)個(gè)詞法單元(token)
- 語法分析:將詞法單元根據(jù)一定規(guī)則組裝成抽象語法樹
通過 javascript-ast 網(wǎng)站撩嚼,可以大概了解 代碼生成的 Tokens 以及 AST大致的樣子停士。
- 詞法分析:將JavaScript代碼解析成一個(gè)個(gè)詞法單元(token)
例如let a = 2;
,通常會(huì)被分解為下面這些詞法單元 let
完丽、a
恋技、=
、2
逻族、;
空格是否會(huì)被當(dāng)做詞法單元取決于空格在這門語言中是否會(huì)具有意義蜻底。
語法分析:將詞法單元根據(jù)一定規(guī)則組裝成 AST
let a = 2;
console.log(a);
我們可以看到生成的AST結(jié)構(gòu)如下:
高級(jí)語言是開發(fā)者可以理解的語言,編譯器和解釋器理解不了聘鳞。所以無論你使用的是解釋型語言還是編譯型語言薄辅,在編譯過程中,它們都會(huì)生成一個(gè) AST抠璃。當(dāng)生成 AST之后站楚,編譯器/解析器后續(xù)的工作都要依靠 AST而不是源碼。
AST是一個(gè)非常重要數(shù)據(jù)結(jié)構(gòu)搏嗡,比如Babel的工作原理就是: ES6 的代碼解析成 AST -> 將 ES6 的 AST 轉(zhuǎn)換成 ES5 的AST -> 將 ES5的 AST 轉(zhuǎn)成 ES5的代碼窿春。Babel的相關(guān)文章推薦 深入淺出 Babel 上篇:架構(gòu)和原理 + 實(shí)戰(zhàn)拉一;我們使用的 Eslint(檢查JavaScript編寫規(guī)范的插件) 的檢測(cè)流程也是先將源碼轉(zhuǎn)換成 AST, 然后利用 AST 來檢查代碼規(guī)范的問題
生成字節(jié)碼
JavaScript引擎通過解釋器來將 AST 轉(zhuǎn)換成字節(jié)碼旧乞,字節(jié)碼是無法直接執(zhí)行的蔚润,需要將其轉(zhuǎn)為機(jī)器碼才能直接執(zhí)行。V8早期的時(shí)候良蛮,是直接將AST轉(zhuǎn)成機(jī)器碼的抽碌,后來因?yàn)?V8 需要消耗大量的內(nèi)存來存放轉(zhuǎn)換后的機(jī)器碼,導(dǎo)致嚴(yán)重的內(nèi)存占用問題决瞳。為了解決這個(gè)問題,引入 了字節(jié)碼左权。字節(jié)碼是比機(jī)器碼輕量得多的代碼皮胡。
字節(jié)碼是介于 AST 和機(jī)器碼之間的一種代碼。但是與特定類型的機(jī)器碼無關(guān)赏迟,字節(jié)碼需要通過解釋器將其轉(zhuǎn)換成機(jī)器碼后才能執(zhí)行屡贺。
執(zhí)行代碼
生成字節(jié)碼之后,就到了解釋和執(zhí)行字節(jié)碼階段了锌杀,
監(jiān)聽熱點(diǎn)代碼并優(yōu)化為二進(jìn)制機(jī)器碼
解釋器會(huì)逐條執(zhí)行字節(jié)碼甩栈,(解釋器除了負(fù)責(zé)生成字節(jié)碼,還會(huì)負(fù)責(zé)解釋執(zhí)行機(jī)器碼) 如果發(fā)現(xiàn)一段代碼重復(fù)執(zhí)行多次糕再,就會(huì)它記為熱點(diǎn)代碼(HotSpot)量没,V8會(huì)將這段熱點(diǎn)代碼提交給優(yōu)化編輯器,優(yōu)化編輯器會(huì)在后臺(tái)將字節(jié)碼編譯為二進(jìn)制代碼突想,然后在對(duì)編譯后的二進(jìn)制代碼執(zhí)行優(yōu)化操作殴蹄,并保存下來。保存下來的機(jī)器碼的作用和緩存很類似猾担,當(dāng)解釋器再次遇到相同的內(nèi)容時(shí)袭灯,就可以直接執(zhí)行保存下來的機(jī)器碼。
這樣代碼執(zhí)行得越久绑嘹,執(zhí)行效率就會(huì)越快稽荧,因?yàn)闀?huì)有越來越多的字節(jié)碼被標(biāo)記為 熱點(diǎn)代碼,遇到他們就可以直接執(zhí)行工腋,而不用轉(zhuǎn)成機(jī)器碼姨丈。
反優(yōu)化生成的二進(jìn)制機(jī)器碼
JavaScript是一種非常靈活的動(dòng)態(tài)語言,對(duì)象的結(jié)構(gòu)和屬性在運(yùn)行時(shí)任意被改變夷蚊,而經(jīng)過優(yōu)化后的代碼只能針對(duì)某種固定結(jié)構(gòu)构挤。一旦在執(zhí)行過程中,對(duì)象的結(jié)構(gòu)被動(dòng)態(tài)修改了惕鼓,那么優(yōu)化后的代碼會(huì)變成無效的代碼筋现,這時(shí)候優(yōu)化編輯器就需要執(zhí)行反優(yōu)化操作,經(jīng)過反優(yōu)化的代碼下次執(zhí)行時(shí)就會(huì)回退到解釋器解釋執(zhí)行。
字節(jié)碼的執(zhí)行是需要配合編譯器和解釋器的(這種技術(shù)稱為即時(shí)編譯 JIT)所以之前說 JS是一種解釋型語言并不準(zhǔn)確矾飞。
總結(jié)
整個(gè)過程如下面流程圖所示: