基本概念
- 編譯器帮辟,解釋器
- 抽象語(yǔ)法樹(shù)
- 字節(jié)碼和機(jī)器碼
編譯器和解釋器
計(jì)算機(jī)不能直接理解高級(jí)語(yǔ)言,只能直接理解機(jī)器語(yǔ)言水由,所以必須要把高級(jí)語(yǔ)言翻譯成機(jī)器語(yǔ)言首昔,計(jì)算機(jī)才能執(zhí)行高級(jí)語(yǔ)言編寫(xiě)的程序寡喝。根據(jù)語(yǔ)言的執(zhí)行流程,可以把語(yǔ)言分成編譯型語(yǔ)言和解釋型語(yǔ)言勒奇。
編譯型語(yǔ)言:程序在執(zhí)行之前需要一個(gè)專門(mén)的編譯過(guò)程预鬓,把程序編譯成 為機(jī)器語(yǔ)言的文件,運(yùn)行時(shí)不需要重新翻譯赊颠,直接使用編譯的結(jié)果就行了格二。程序執(zhí)行效率高,依賴編譯器竣蹦,跨平臺(tái)性差些顶猜。如C、C++痘括、go等.
解釋型語(yǔ)言: 程序不需要編譯长窄,程序在運(yùn)行時(shí)才翻譯成機(jī)器語(yǔ)言(所以執(zhí)行前需要環(huán)境中安裝了解釋器)仙粱,每執(zhí)行一次都要翻譯一次忠聚。因此效率比較低。效率比較低弧关,依賴解釋器翰舌,跨平臺(tái)性好嚣潜。
編譯型與解釋型,兩者各有利弊椅贱, 不能一概而論懂算。前者由于程序執(zhí)行速度快,同等條件下對(duì)系統(tǒng)要求較低庇麦,因此像開(kāi)發(fā)操作系統(tǒng)计技、大型應(yīng)用程序、數(shù)據(jù)庫(kù)系統(tǒng)等時(shí)都采用它山橄,像C/C++垮媒、Pascal/Object Pascal(Delphi)等都是編譯語(yǔ)言,而一些網(wǎng)頁(yè)腳本驾胆、服務(wù)器腳本及輔助開(kāi)發(fā)接口這樣的對(duì)速度要求不高涣澡、對(duì)不同系統(tǒng)平臺(tái)間的兼容性有一定要求的程序則通常使用解釋性語(yǔ)言贱呐,如JavaScript丧诺、VBScript、Perl奄薇、Python驳阎、Ruby、MATLAB 等等。
我們都知道 JavaScript 存在變量提升呵晚,在函數(shù)作用域內(nèi)的任何變量的聲明都會(huì)被提升到頂部并且值為 undefined蜘腌。
所以JS引擎好像對(duì)同一個(gè)腳本執(zhí)行了兩次,第一次完成所有聲明饵隙,然后第二次才執(zhí)行代碼撮珠?還是先編譯整個(gè)代碼然后運(yùn)行它?這兩種都不對(duì)金矛。
其實(shí)變量聲明不過(guò)只執(zhí)行上下文的小把戲芯急。在執(zhí)行任何語(yǔ)句之前,解釋器就要從創(chuàng)建執(zhí)行上下文后已經(jīng)存在的作用域中找到變量的值驶俊。
抽象語(yǔ)法樹(shù)
抽象語(yǔ)法樹(shù)(Abstract Syntax Tree娶耍,AST),或簡(jiǎn)稱語(yǔ)法樹(shù)(Syntax tree)饼酿,是源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示榕酒。它以樹(shù)狀的形式表現(xiàn)編程語(yǔ)言的語(yǔ)法結(jié)構(gòu),樹(shù)上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)故俐。之所以說(shuō)語(yǔ)法是“抽象”的想鹰,是因?yàn)檫@里的語(yǔ)法并不會(huì)表示出真實(shí)語(yǔ)法中出現(xiàn)的每個(gè)細(xì)節(jié)。比如购披,嵌套括號(hào)被隱含在樹(shù)的結(jié)構(gòu)中杖挣,并沒(méi)有以節(jié)點(diǎn)的形式呈現(xiàn);而類似于 if-condition-then 這樣的條件跳轉(zhuǎn)語(yǔ)句刚陡,可以使用帶有兩個(gè)分支的節(jié)點(diǎn)來(lái)表示惩妇。
字節(jié)碼和機(jī)器碼
字節(jié)碼(Byte-code):是一種包含執(zhí)行程序、由一序列 op 代碼/數(shù)據(jù)對(duì)組成的二進(jìn)制文件筐乳。字節(jié)碼是一種中間碼歌殃,它比機(jī)器碼更抽象。
機(jī)器碼 (Machine-code):計(jì)算機(jī)直接使用的程序語(yǔ)言蝙云,其語(yǔ)句就是機(jī)器指令碼氓皱,機(jī)器指令碼是用于指揮計(jì)算機(jī)應(yīng)做的操作和操作數(shù)地址的一組二進(jìn)制數(shù)。
JavaScript代碼執(zhí)行過(guò)程
- 生成AST(抽象語(yǔ)法樹(shù))
- 生成字節(jié)碼
- 執(zhí)行代碼
生成AST
生成AST的步驟可以拆分成以下兩個(gè)小步驟:
- 詞法分析:將JavaScript代碼解析成一個(gè)個(gè)詞法單元(token)
- 語(yǔ)法分析:將詞法單元根據(jù)一定規(guī)則組裝成抽象語(yǔ)法樹(shù)
通過(guò) javascript-ast 網(wǎng)站勃刨,可以大概了解 代碼生成的 Tokens 以及 AST大致的樣子波材。
- 詞法分析:將JavaScript代碼解析成一個(gè)個(gè)詞法單元(token)
例如let a = 2;
,通常會(huì)被分解為下面這些詞法單元 let
身隐、a
廷区、=
、2
贾铝、;
空格是否會(huì)被當(dāng)做詞法單元取決于空格在這門(mén)語(yǔ)言中是否會(huì)具有意義隙轻。
- 語(yǔ)法分析:將詞法單元根據(jù)一定規(guī)則組裝成 AST
let a = 2;
console.log(a);
我們可以看到生成的AST結(jié)構(gòu)如下:
高級(jí)語(yǔ)言是開(kāi)發(fā)者可以理解的語(yǔ)言埠帕,編譯器和解釋器理解不了。所以無(wú)論你使用的是解釋型語(yǔ)言還是編譯型語(yǔ)言玖绿,在編譯過(guò)程中敛瓷,它們都會(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編寫(xiě)規(guī)范的插件) 的檢測(cè)流程也是先將源碼轉(zhuǎn)換成 AST, 然后利用 AST 來(lái)檢查代碼規(guī)范的問(wèn)題
生成字節(jié)碼
JavaScript引擎通過(guò)解釋器來(lái)將 AST 轉(zhuǎn)換成字節(jié)碼苍姜,字節(jié)碼是無(wú)法直接執(zhí)行的牢酵,需要將其轉(zhuǎn)為機(jī)器碼才能直接執(zhí)行。V8早期的時(shí)候衙猪,是直接將AST轉(zhuǎn)成機(jī)器碼的馍乙,后來(lái)因?yàn)?V8 需要消耗大量的內(nèi)存來(lái)存放轉(zhuǎn)換后的機(jī)器碼,導(dǎo)致嚴(yán)重的內(nèi)存占用問(wèn)題垫释。為了解決這個(gè)問(wèn)題丝格,引入 了字節(jié)碼。字節(jié)碼是比機(jī)器碼輕量得多的代碼棵譬。
字節(jié)碼是介于 AST 和機(jī)器碼之間的一種代碼显蝌。但是與特定類型的機(jī)器碼無(wú)關(guān),字節(jié)碼需要通過(guò)解釋器將其轉(zhuǎn)換成機(jī)器碼后才能執(zhí)行订咸。
執(zhí)行代碼
生成字節(jié)碼之后曼尊,就到了解釋和執(zhí)行字節(jié)碼階段了,
監(jiān)聽(tīng)熱點(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)化操作趾唱,并保存下來(lái)涌乳。保存下來(lái)的機(jī)器碼的作用和緩存很類似,當(dāng)解釋器再次遇到相同的內(nèi)容時(shí)甜癞,就可以直接執(zhí)行保存下來(lái)的機(jī)器碼夕晓。
這樣代碼執(zhí)行得越久,執(zhí)行效率就會(huì)越快带欢,因?yàn)闀?huì)有越來(lái)越多的字節(jié)碼被標(biāo)記為 熱點(diǎn)代碼运授,遇到他們就可以直接執(zhí)行,而不用轉(zhuǎn)成機(jī)器碼乔煞。
反優(yōu)化生成的二進(jìn)制機(jī)器碼
JavaScript是一種非常靈活的動(dòng)態(tài)語(yǔ)言吁朦,對(duì)象的結(jié)構(gòu)和屬性在運(yùn)行時(shí)任意被改變,而經(jīng)過(guò)優(yōu)化后的代碼只能針對(duì)某種固定結(jié)構(gòu)渡贾。一旦在執(zhí)行過(guò)程中逗宜,對(duì)象的結(jié)構(gòu)被動(dòng)態(tài)修改了,那么優(yōu)化后的代碼會(huì)變成無(wú)效的代碼空骚,這時(shí)候優(yōu)化編輯器就需要執(zhí)行反優(yōu)化操作纺讲,經(jīng)過(guò)反優(yōu)化的代碼下次執(zhí)行時(shí)就會(huì)回退到解釋器解釋執(zhí)行。
字節(jié)碼的執(zhí)行是需要配合編譯器和解釋器的(這種技術(shù)稱為即時(shí)編譯 JIT)所以之前說(shuō) JS是一種解釋型語(yǔ)言并不準(zhǔn)確囤屹。
總結(jié)
整個(gè)過(guò)程如下面流程圖所示:
參考
- JavaScript到底是解釋型語(yǔ)言還是編譯型語(yǔ)言?
- javascript-ast
- 極客時(shí)間-瀏覽器工作原理與實(shí)踐熬甚。