由 Babel 理解前端編譯原理
背景
我們知道編程語言主要分爲「編譯型語言」和「解釋型語言」,編譯型語言是在代碼運行前編譯器將編程語言轉換成機器語言,運行時不需要重新翻譯,直接使用編譯的結果就行了。而解釋型語言也是需要將編程語言轉換成機器語言,但是是在運行時轉換的。
通常我們都將 JavaScript 歸類爲「解釋型語言」,以至於很多人都誤以爲前端代碼是不需要編譯的,但其實 JavaScript 引擎進行編譯的步驟和傳統的編譯語言非常相似,只不過與傳統的編譯語言不同,它不是提前編譯的。並且隨着現代瀏覽器和前端領域的蓬勃發展,編譯器在前端領域的應用越來越廣泛,就日常工作而言,包括但不限於以下幾個方面:
-
v8 引擎、typescript 編譯器(tsc)
-
webpack loader 編譯器(acorn[19]),babel、SWC 等編譯工具。
-
angular、Vue 等框架的模板編譯器、jsx
作爲前端開發,我們沒必要對這些編譯器或者底層的編譯原理了如指掌,但是如果能對編譯原理有一些基本的認識,也能夠對今後的日常開發很有幫助。本文就帶領大家學習下編譯原理的一些基本概念,並以 Babel 爲例講解下前端編譯的基本流程。
概述
我們先來回顧下編譯原理的基本知識,從宏觀上來說,編譯本質上是一種轉換技術,從一門編程語言轉換成另一門編程語言,或者從高級語言轉換成低級語言,或者從高級語言到高級語言,所謂的高級語言和低級語言主要是指下面的區分:
-
高級語言:有很多用於描述邏輯的語言特性,比如分支、循環、函數、面向對象等,接近人的思維,可以讓開發者快速的通過它來表達各種邏輯。比如 c++、javascript。
-
低級語言:與硬件和執行細節有關,會操作寄存器、內存,具體做內存與寄存器之間的複製,需要開發者理解熟悉計算機的工作原理,熟悉具體的執行細節。比如彙編語言、機器語言。
無論是怎樣的編譯過程,基本都會是下面的一個過程:
C/C++
語言經過編譯得到二進制的機器碼,然後交給操作系統,例如當我們運行 tsc 命令就會將 TS 代碼編譯爲 js 代碼,再比如執行 babel 命令會將 es6+ 的代碼編譯爲指定目標 (es5) 的 js 代碼。
一般來說,整個編譯過程主要分爲兩個階段:編譯 前端和編譯後端,大致分爲下面的幾個過程:
上下文無關文法
前面提到編譯器會根據「約定的編譯規則」進行編譯,這裏「約定的編譯規則」就是指「上下文無關文法(CFG)[20]」。CFG 用於在理論上的形式化定義一門語言的語法,或者說,用於系統地描述程序設計語言的構造(比如表達式和語句)。
實際上,幾乎所有程序設計語言都是通過上下文無關文法來定義的,與正則表達式比較像,但是比正則表達式功能更強大,它能表達非常複雜的文法,比如 C 語言語法用正則表達式來表示不可能做到,但是可以用 CFG 的一組規則來表達。
要理解上下文無關文法,需要先理解下面幾個概念:
-
終結符:可以理解爲基礎符號,詞法符號,是不可替代的,是固定存在的,不能通過文法規則生成的。
-
非終結符:句法變量,是可以替代的
-
產生式規則:語法是由終結符集、非終結符集和產生式規則共同組成。產生式規則定義了符號之間如何轉換替代。規則的左側是規則頭,即非終結符,是可以被替代的符號;右側是產生式,是具體的內容。
例如下面 a, b, c, d 爲終結符(用小寫表示),(S, A) 爲非終結符(用大寫表示)。S -> cAd, A -> a | ab 表示產生式規則。S->cAd,然後可以產生 "cad",“cabd” 等符合文法的內容
S -> cAd
A -> a | ab
上下文無關文法比較抽象,不是這裏學習的重點,感興趣的話可以專門深入瞭解下,這裏知乎上也有篇回答可以參考下 應該如何理解「上下文無關文法」?[21]
下面我們來簡單模擬下如何用 CFG 來定義一門語言的語法, 我們假設一個極其簡單的語言,這個語言只能像 js 那樣聲明整數型常量,以及聲明不接受任何參數且只能直接返回常量加法的箭頭函數。
const a = 10
const b = 20
const c = () => a + b
這個語言的文法表達如下:
program :: statement+
statement :: declare | func
declare :: CONST VARIABLE ASSIGN INTEGER
func :: CONST LPAREN RPAREN ARROW expression
expression :: VARIABLE + VARIABLE
CONST :: "const"
ASSIGN :: "="
LPAREN :: "("
RPAREN :: ")"
ARROW :: "=>"
INTEGER :: \d+
VARIABLE :: \w[\w\d$_]*
可以看出,整個文法的表達,涵蓋了很多正則表達式的概念。該表達是一種自頂向下的規範:
-
首先入口約束了程序(program)是由一條(及以上)的表達式(statement)構成,
-
而表達式又可以由聲明語句(declare)或函數語句(func)構成。
-
聲明語句依次由 const、關鍵字、符號 =、整數 從左到右排列構成。
-
整數的定義則直接使用正則表達式來約束。函數語句也是類似。
大家可以觀察到,上述的文法分成了上下兩個大的部分。上半部分定義了語句以及由語句遞歸構造的表達,通常稱爲「語法規則」(grammar rules);下半部分定義了可通過排列構成語句的基本詞彙,通常稱爲「詞法規則」(lexer rules)。
在實踐中,詞法規則往往沒有單獨羅列,而是直接寫入到語法規則中。比如上述文法可簡化爲:
program :: statement+
statement :: declare | func
declare :: "const" variable "=" integer
func :: "const" variable "=" "(" ")" "=>" expression
expression :: variable + variable
variable :: \w[\w\d$_]*
integer :: \d+
上面的文法表達形式叫做 BNF[22],它是用來描述上下文無關文法的一種描述語言,形式爲<符號> ::= <使用符號的表達式>
,這裏的 <符號> 是非終結符,而表達式由一個符號序列,或用指示選擇的豎槓 '|' 分隔的多個符號序列構成,每個符號序列整體都是左端的符號的一種可能的替代。從未在左端出現的符號就是終結符。
有了 BNF,我們就可以實現語言文法的具體化、公式化,甚至可以自己實現一個語言來解決特定領域的問題。再來看個例子,用 BNF 來描述四則運算:
result ::= number ("+"|"-") exp | number // 非終結符
exp ::= number("*"|"/") exp | number // 非終結符
number ::= [0-9]+ // 終結符
另外,我們也可以窺探下 ECMA 和 JSON 的 BNF:
-
JSON Schema 的 BNF:Syntax - JSON Schema[23]
-
ECMA 的 BNF:function&class bnf[24]
編譯器工作流程
接下來我們就相對深入的來看一下編譯的各個階段。
詞法分析
就像我們學習一門語言的第一步是學習單詞一樣,編譯器識別源代碼的第一步就是要要進行分詞,識別出每一個單詞或符號。這個階段,詞法分析器會將源代碼拆分成一組 token 串:
-
首先,通過對源代碼的字符串從左到右進行掃描,以空白字符(空格、換行、製表符等)爲分隔符,拆分爲一個個無類型的 token 。
-
其次,再根據詞法規則,利用「有限狀態機 [25]」對第一步拆分的 Token 進行字符串模式匹配,以識別每一個 Token 的類型(v8 token.h[26])。
一般而言,token 是一個有類型和值的數據結構,而 token 流簡單理解可以是 token 數組。以下面一行代碼爲例:
const name = 'xujianglong';
// 根據 js 的語法規則, 大致會生成如下的 token 流
[
{ type: "CONST", value: "const" },
{ type: "IDENTIFIER", value: "name" },
{ type: "ASSIGN", value: "=" },
{ type: "STRING", value: "xujianglong" },
{ type: "SEMICOLON", value: ";" },
]
那麼有限狀態機是個什麼概念呢?它是怎麼把字符串代碼轉化爲 token 的呢?
首先我們想一下,詞法描述的是最小的單詞格式,比如上面例子的那一行代碼爲例,利用空白字符拆分成這幾個 token:['const','name','=','xujianglong',';']
,但是怎麼去識別每種 token 的類型呢,最簡單粗暴的方式我們可以寫個 if else
語句或者寫個正則,但是這樣貌似不太優雅且不容易維護,而使用狀態機是許多編程語言都使用的方式。
有限狀態機(英語:finite-state machine,縮寫:FSM)又稱有限狀態自動機(英語:finite-state automation,縮寫:FSA),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行爲的數學計算模型。
如圖所示,用戶從其他狀態機進入 S1 狀態機,如果用戶輸入 1,則繼續進入 S1 狀態機,如果用戶輸入了 0,則進入下一個狀態機 S2。在 S2 狀態機中,如果用戶繼續輸入 1,則繼續進入 S2 狀態機,如果用戶輸入了 0,則回到 S1 狀態機。這是一個循環的過程。
聽起來有點抽象,對比到代碼分詞中來說,我們可以把每個單詞的處理過程當成一種狀態,將整體的輸入(源代碼)按照每個字符依次去讀取,根據每次讀取到的字符來更改當前的狀態,每個 token 識別完了就可以拋出來。我們舉個簡單的四則運算的例子:10 + 20 - 30
NUMBER
代表數值,ADD
代表加號,SUB
代表減號:
-
當分析到 "1" 時,因爲本次輸入我們需要改變狀態機內部狀態爲
NUMBER
,繼續迭代下一個字符 “0”,此時因爲 "1" 和 "0" 是一個整體可以不被分開的。 -
當分析到 "+" 時,狀態機中輸入爲 “+”, 顯然 “+” 是一個運算符號,它並不能和上一次的 “10” 拼接在一起。所以此時狀態改變,我們將上一次的 currentToken 也就是 "10" 推入 tokens 中,同時改變狀態機狀態爲
ADD
。 -
依次類推,最終會輸出如下 tokens 數組:
[
{ type: "NUMBER", value: "10" },
{ type: "ADD", value: "+" },
{ type: "NUMBER", value: "20" },
{ type: "SUB", value: "-" },
{ type: "NUMBER", value: "30" },
]
語法分析
在這個階段,語法分析器(parser)會將詞法分析中得到的 token 數組轉化爲 抽象語法樹 AST 。比如前面定義變量的那行代碼可以在這個在線工具 AST explorer[27] 中查看生成的 AST:
{
"type": "Program",
"start": 0,
"end": 28,
"body": [
{
"type": "VariableDeclaration",
"start": 1,
"end": 28,
"declarations": [
{
"type": "VariableDeclarator",
"start": 7,
"end": 27,
"id": {
"type": "Identifier",
"start": 7,
"end": 11,
"name": "name"
},
"init": {
"type": "Literal",
"start": 14,
"end": 27,
"value": "xujianglong",
"raw": "'xujianglong'"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
對於 JavaScript 語言來說,AST 也有一套約定的規範:GitHub - estree/estree: The ESTree Spec[28],社區稱之爲 estree,藉助這個規範,整個前端社區的一些工具便可以產出一套統一的數據格式而無需關心下游,下游的消費類工具統一使用這個統一的格式進行處理而無需關心上游,這樣就做到了上下游的解耦。以 webpack 爲例,其底層是 acorn[29] 工具,acorn 會把 js 源碼轉化爲上述的標準 estree,而 webpack 作爲下游便可以消費該 estree,比如遍歷,提取和分析 require/import 依賴,轉換代碼並輸出。
生成 AST 的過程需要遵循語法規則(用上下文無關文法表示),在上面代碼中,我們用到了「VariableDeclaration」,其語法規則可以表示爲:
VariableDeclaration :: Kind Identifier Init?;
Kind :: Const | Let | Var;
Init :: '=' Expression | Identifier | Literal;
Expression :: BinaryExpression | ConditionalExpression | ...;
Literal :: StringLiteral | ...;
有了語法規則之後,我們就需要思考編譯器是如何將 token 流,在語法規則的約束下,轉換成 AST 的呢?生成 AST 也主要是兩大方向:
-
一是將文法規則的約束硬編碼到編譯器的代碼邏輯中,這種是特定語言的編譯器使用的常見方案,這種方案往往是人工編寫 parse 代碼,對輸入源碼的各種錯誤和異常可以更細緻地報告和處理。比如前面提到的 arorn,以及 tsc,babel,以及熟悉的 vue,angular 的模板編譯器等,都主要是這種方法。
-
二是使用自動生成工具將文法規則直接轉換成語法 parse 代碼。這種更常用於非特定的編程語言,比如一些業務中自定義的簡單但易變的語法,或僅僅只是字符串文本的複雜處理規則。
我們這裏以第一種方式最基礎的 「遞歸下降算法」(遞歸下降的編譯技術,是業界最常使用的手寫編譯器實現編譯前端的技術之一,感興趣可以專門去深入研究下) 爲例,簡單描述示例代碼生成 AST 的過程:
嘗試匹配 VariableDeclaration
匹配到 Const
匹配到 Identifier
嘗試匹配 Init,遞歸下降
匹配到 '='
嘗試匹配 Expression,遞歸下降
匹配失敗,回溯
匹配到 Literal,回溯
VariableDeclaration 匹配成功,構造相應類型節點插入 AST
語義分析
並不是所有的編譯器都有語義分析,比如 Babel 就沒有。不過對於其它大部分編程語言(包括 TypeScript)的編譯器來說,是有語義分析這一步驟的,特別是靜態類型語言,類型檢查就屬於語義分析的其中一個步驟
語義分析階段,編譯器開始對 AST 進行一次或多次的遍歷,檢查程序的語義規則。主要包括聲明檢查和類型檢查,如上一個賦值語句中,就需要檢查:
-
語句中的變量 name 是否被聲明過
-
const 類型變量是否被改變
-
加號運算的兩個操作數的類型是否匹配
-
函數的參數數量和類型是否與其聲明的參數數量及類型匹配
語義檢查的步驟和人對源代碼的閱讀和理解的步驟差不多,一般都是在遍歷 AST 的過程中,遇到變量聲明和函數聲明時,則將變量名 -- 類型、函數名 -- 返回類型 -- 參數數量及類型等信息保存到符號表裏,當遇到使用變量和函數的地方,則根據名稱在符號表中查找和檢查,查找該名稱是否被聲明過,該名稱的類型是否被正確的使用等等。
語義檢查時,也會對語法樹進行一些優化,比如將只含常量的表達式先計算出來,如:
a = 1 + 2 * 9;
會被優化成:
a = 19;
語義分析完成後,源代碼的結構解析就已經完成了,所有編譯期錯誤都已被排除,所有使用到的變量名和函數名都綁定到其聲明位置(地址)了,至此編譯器可以說是真正理解了源代碼,可以開始進行代碼生成和代碼優化了。
中間代碼生成與優化
一般的編譯器並不直接生成目標代碼,而是先生成某種中間代碼,然後再生成目標代碼。之所以先生成中間代碼,主要有以下幾個原因:
-
爲了降低編譯器開發的難度,將高級語言翻譯成中間代碼、將此中間代碼再翻譯成目標代碼的難度都比直接將高級語言翻譯成目標代碼的難度要低。
-
爲了增加編譯器的模塊化、可移植性和可擴展性,一般來說,中間代碼既獨立於任何高級語言,也獨立於任何目標機器架構,這就爲開發出適應性廣泛的編譯器提供了媒介
-
爲了代碼優化,一般來說,計算機直接生成的代碼比人手寫的彙編要龐大、重複很多,計算機科學家們對一些具有固定格式的中間代碼的進行大量的研究工作,提出了很多廣泛應用的、效率非常高的優化算法,可以對中間代碼進行優化,比直接對目標代碼進行優化的效果要好很多。
JavaScript 的編譯器 v8 引擎早期是沒有中間代碼生成的,直接從 AST 生成本地可執行的代碼,但由於缺少了轉換爲字節碼這一中間過程,也就減少了優化代碼的機會。爲了提高性能, v8 開始採用了引入字節碼的架構,先把 AST 編譯爲字節碼,再通過 JIT 工具轉換成本地代碼。
v8 引擎的編譯過程這裏不做過多介紹,後續可作爲單獨的分享。
-
V8 引擎詳解(三)——從字節碼看 V8 的演變 [30]
-
理解 V8 的字節碼「譯」[31]
生成目標代碼
有了中間代碼後,目標代碼的生成是相對容易的,因爲中間代碼在設計的時候就考慮到要能輕鬆生成目標代碼。不同的高級語言的編譯器生成的目標代碼不一樣,如:
-
C、C++、Go,目標代碼都是彙編語言(可能目標代碼不一樣),在經過彙編器最終得到機器碼;
-
Java 經過 javac 編譯後,生成字節碼,只經過了編譯的詞法分析、語法分析、語義分析和中間代碼生成,運行時再由解釋器逐條將字節碼解釋爲機器碼來執行;
-
JavaScript,在運行時經過整個編譯流程,不過也是先生成字節碼,然後解析器解析字節碼執行;
-
至於 webpack、babel 這些前端工具則是最終編譯成相應的 js 代碼。
Babel 的編譯流程
前面我們提到,一般編譯器是指高級語言到低級語言的轉換工具,特殊地像前端的一些工具類型的轉換,如 ts 轉 js,js 轉 js 等這些都是高級語言到高級語言的轉換工具,通常被叫做轉換編譯器,簡稱轉譯器 (Transpiler),所以 Babel 就是是一個 JavaScript 編譯器。
Babel 主要用於將採用 ECMAScript 2015+ 語法編寫的代碼轉換爲向後兼容的 JavaScript 語法,並且還可以把目標環境不支持的 api 進行 polyfill。以便能夠運行在當前和舊版本的瀏覽器或其他環境中。例如下面的例子:
// Babel 輸入:ES2015 箭頭函數
[1, 2, 3].map(n => n + 1);
// Babel 輸出:ES5 語法實現的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
Babel 的工作流程可分爲如下幾個步驟:
下面詳細介紹下 babel 工作流程的各個階段。
parse(解析)
parse 這個階段將原始代碼字符串轉爲 AST 樹,parse 廣義上來說包括了我們前面編譯流程中講到的 詞法分析、語法分析這兩個階段。parse 過程中會有一些 babel 插件,讓 babel 可以解析出更多的語法,比如 jsx。
parse(sourceCode) => AST
parse 階段,主要通過 @babel/parser
這個包進行轉換,之前叫 babylon,是基於 acorn 實現的,擴展了很多語法,可以支持 esnext(現在支持到 es2020)、jsx、flow、typescript 等語法的解析,其中 jsx、flow、typescript 這些非標準的語法的解析需要指定語法插件。
我們可以手動調用下 parser 的方法進行轉換,便會得到一份 AST:
const parser = require("@babel/parser");
const fs = require('fs');
const code = `function square(n) {
return n * n;};`
const result = parser.parse(code)
console.log(result);
最終輸出的 AST 如下所示:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
可以看到 AST 的每一層都擁有相似的結構,這樣的每一層結構也被叫做 節點(Node), 一個 AST 可以由單一的節點或是成百上千個節點構成,它們組合在一起可以描述用於靜態分析的程序語法。每個節點都有個字符串的 type 類型,用來表示節點的類型,babel 中定義了包含所有 JavaScript 語法的類型,如:
-
聲明語句:如
FunctionDeclaration
、VaraibaleDeclaration
、ClassDeclaration
等聲明; -
標識符:
Identifier
,變量或函數參數。 -
字面量:
StringLiteral
、NumbericLiteral
、BooleanLiteral
等字面量類型; -
語句:
WhileStatement
、ReturnStatement
等語句; -
表達式:
FunctionExpression
、BinaryExpression
、AssignmentExpression
等。
所有的這些節點通過嵌套形成了 AST 樹,例如一個變量賦值語句形成的樹形結構如下所示:
transform(轉化)
transform 階段主要是對上一步 parse 生成的 AST 進行深度優先遍歷,從而對於匹配節點進行增刪改查來修改樹形結構。在 babel 中會用所配置的 plugin 或 presets 對 AST 進行修改後,得到新的 AST,我們的 babel 插件大部分用於這個階段。
transform(AST, BabelPlugins) => newAST
babel 中通過 @babel/traverse
這個包來對 AST 進行遍歷,找出需要修改的節點再進行轉換,這個過程有點類似我們操作 DOM 樹。當我們談及 “進入” 一個節點,實際上是說我們在訪問它們, 之所以使用這樣的術語是因爲有一個訪問者模式(visitor)[32] 的概念。訪問者 visitor 是一個用於 AST 遍歷的跨語言的模式。簡單的說它就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法。
visitor 是一個由各種 type
或者是 enter
和 exit
組成的對象,完成某種類型節點的 "進入" 或 "退出" 兩個步驟則爲一次訪問,在其中可以定義在遍歷 AST 的過程中匹配到某種類型的節點後該如何操作,目前支持的寫法如下:
traverse(ast, {
/* VisitNodeObject */
enter(path, state) {},
exit(path, state) {},
/* [Type in t.Node["type"]] */
Identifier(path, state) {}, // 進入 Identifier(標識符節點) 節點時調用
StringLiteral: { // 進入 StringLiteral(字符串節點) 節點時調用
enter(path, state) {}, // 進入該節點時的操作
exit(path, state) {}, // 離開該節點時的操作
},
'FunctionDeclaration|VariableDeclaration'(path, state) {}, // // 進入 FunctionDeclaration 和 VariableDeclaration 節點時調用
})
訪問一個節點的過程如下:
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const fs = require('fs');
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
const newAst = traverse(ast, {
enter(path) {
if (path.isIdentifier({
name: "n"
})) {
path.node.name = "x";
}
},
FunctionDeclaration: {
enter() {
console.log('enter function declaration')
},
exit() {
console.log('exit function declaration')
}
}
});
上面的例子中,通過識別標識符把 "n" 換成了 "x",其中的 path 是遍歷過程中的路徑,會保留上下文信息,有很多屬性和方法,可以在訪問到指定節點後,根據 path 進行自定義操作,比如:
-
path.node
指向當前 AST 節點,path.parent
指向父級 AST 節點; -
path.getSibling、path.getNextSibling、path.getPrevSibling
獲取兄弟節點; -
path.isxxx
判斷當前節點是不是 xx 類型; -
path.insertBefore、path.insertAfter
插入節點; -
path.replaceWith、path.replaceWithMultiple、replaceWithSourceString
替換節點; -
path.skip
跳過當前節點的子節點的遍歷,path.stop
結束後續遍。
有了 @babel/traverse
我們可以在 tranform 階段做很多自定義的事情,例如刪除 console.log
語句,在特定的地方插入一些表達式等等,從而影響輸出結果。我們再舉一個例子,在 console
語句中增加位置信息的輸出,形如:console.log('[18,0]', 111)
。
import * as t from "@babel/types"; // 用來創建一些 AST 和判斷 AST 的類型
traverse(ast, {
visitor: {
CallExpression(path, state) {
const callee = path.node.callee;
if (
callee.object.name === 'console' &&
['log', 'info', 'error'].includes(callee.property.name)
) {
const { line, column } = path.node.loc.start;
const locationNode = types.stringLiteral( `[ ${line} , ${column} ]` );
path .node.arguments.unshift(locationNode); }
},
},
})
generate(生成)
AST 轉換完之後就要輸出目標代碼字符串,這個階段是個逆向操作,用新的 AST 來生成我們所需要的代碼,在生成階段本質上也是遍歷抽象語法樹,根據抽象語法樹上每個節點的類型和屬性遞歸調用從而生成對應的字符串代碼,在 babel 中通過 @babel/generator
包的 api 來實現。
generate(newAST) => newSourceCode
還拿前面的 transform 的代碼舉例,如下所示:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({
name: "n"
})) {
path.node.name = "x";
}
},
});
const output = generate(ast, {}, code);
console.log(output)
// {
// code: 'function square(x) { return x * x;}',
// map: null,
// rawMappings: undefined
// }
由上面代碼可以看到,函數里的變量 "n" 被替換成了 "x"。
總結
編譯原理涉及大量的概念及知識,以實現完整編譯鏈路的 GCC 編譯器來看,單源代碼就 600w 行,足以說明其水深且寬,但編譯原理帶來的價值是巨大的,可以說是編譯原理的進步纔有各種高級語言百花齊放,進而提高軟件行業生產力。Babel 作爲前端工程化領域一個很重要的工具,明白了其編譯流程,對諸如其他工具 v8 引擎、 tsc、jsx 模板等前端方面的編譯原理便可以融匯貫通,觸類旁通。
參考文檔
-
Babel 官網 [33]
-
JavaScript:V8 編譯過程 [34]
-
babel-handbook[35]
❤️ 謝謝支持
以上便是本次分享的全部內容,希望對你有所幫助 ^_^
歡迎關注公衆號 ELab 團隊 收穫大廠一手好文章~
我們來自字節跳動,是旗下大力教育前端部門,負責字節跳動教育全線產品前端開發工作。
參考資料
[1] acorn: https://github.com/acornjs/acorn
[2] 上下文無關文法(CFG): https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E6%97%A0%E5%85%B3%E6%96%87%E6%B3%95
[3] 應該如何理解「上下文無關文法」?: https://www.zhihu.com/question/21833944/answer/307309365
[4] BNF: https://zh.wikipedia.org/wiki/%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F
[5] Syntax - JSON Schema: https://cswr.github.io/JsonSchema/spec/grammar/
[6] function&class bnf: https://tc39.es/ecma262/#sec-ecmascript-language-functions-and-classes
[7] 有限狀態機: https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA
[8] v8 token.h: https://github.com/v8/v8/blob/master/src/parsing/token.h#L56
[9] AST explorer: https://astexplorer.net/
[10] GitHub - estree/estree: The ESTree Spec: https://github.com/estree/estree
[11] acorn: https://github.com/acornjs/acorn
[12] V8 引擎詳解(三)——從字節碼看 V8 的演變: https://juejin.cn/post/6844904152745639949
[13] 理解 V8 的字節碼「譯」: https://zhuanlan.zhihu.com/p/28590489
[14] 訪問者模式(visitor): https://en.wikipedia.org/wiki/Visitor_pattern
[15] Babel 官網: https://babeljs.io/docs/en/
[16] 編譯技術在前端的實踐(一)——編譯原理基礎: https://tech.bytedance.net/articles/7002225912913608735
[17] JavaScript:V8 編譯過程: https://juejin.cn/post/6844903953981767688#heading-0
[18] babel-handbook: https://link.juejin.im/?target=https://github.com/jamiebuilds/babel-handbook
[19] acorn: https://github.com/acornjs/acorn
[20] 上下文無關文法(CFG): https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E6%97%A0%E5%85%B3%E6%96%87%E6%B3%95
[21] 應該如何理解「上下文無關文法」?: https://www.zhihu.com/question/21833944/answer/307309365
[22] BNF: https://zh.wikipedia.org/wiki/%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F
[23] Syntax - JSON Schema: https://cswr.github.io/JsonSchema/spec/grammar/
[24] function&class bnf: https://tc39.es/ecma262/#sec-ecmascript-language-functions-and-classes
[25] 有限狀態機: https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA
[26] v8 token.h: https://github.com/v8/v8/blob/master/src/parsing/token.h#L56
[27] AST explorer: https://astexplorer.net/
[28] GitHub - estree/estree: The ESTree Spec: https://github.com/estree/estree
[29] acorn: https://github.com/acornjs/acorn
[30] V8 引擎詳解(三)——從字節碼看 V8 的演變: https://juejin.cn/post/6844904152745639949
[31] 理解 V8 的字節碼「譯」: https://zhuanlan.zhihu.com/p/28590489
[32] 訪問者模式(visitor): https://en.wikipedia.org/wiki/Visitor_pattern
[33] Babel 官網: https://babeljs.io/docs/en/
[34] JavaScript:V8 編譯過程: https://juejin.cn/post/6844903953981767688#heading-0
[35] babel-handbook: https://link.juejin.im/?target=https://github.com/jamiebuilds/babel-handbook
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Wjw6zdpwcw5LbSmqzkN5RA