Babel 是目前最常用的 JavaScript 編譯器。能夠編譯 JS 程式碼,使得程式碼能夠正常的在舊版本的瀏覽器上面執行;還能夠轉化 JSX 語法,使得 react 寫的程式碼能夠正常執行。
下面,按照編譯原理來實現一個簡單的 JS 程式碼編譯器,實現把 ES6 程式碼轉化成 ES5,以充分了解 Babel 執行原理。
let a = 1
複製程式碼
轉化後
var a = 1
複製程式碼
編譯原理
編譯器的編譯原理大多分為三個階段: 解析、轉換以及程式碼生成
- 解析(Parsing): 將原始程式碼轉化成 AST 抽象樹
- 轉換(Transformation): 對 AST 抽象樹進行處理,變化結構
- 程式碼生成(Code Generation): 把處理後的 AST 抽象樹轉化成程式碼
解析
編譯前,首先要對程式碼進行解析,解析分為兩個階段 詞義分析(Lexical Analysis) 和 語法分析(Syntactic Analysis)
詞義分析
詞義分析是接收原始程式碼進行分詞,最後生成 token。
例如:
let a = 1
詞義分析後結果為:
[ { "type": "Keyword", "value": "let" },
{ "type": "Identifier", "value": "a" },
{ "type": "Punctuator", "value": "=" },
{ "type": "Numeric", "value": "1" } ]
複製程式碼
詞義分析器函式為:
// 解析程式碼,最後返回 tokens
function tokenizer(input) {
// 記錄當前解析到詞的位置
var current = 0
// tokens 用來儲存我們解析的 token
var tokens = []
// 利用迴圈進行解析
while (current < input.length) {
// 提取出當前要解析的字元
var char = input[current]
// 處理符號: 檢查是否是符號
var PUNCTUATOR = /[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/im
if (PUNCTUATOR.test(char)) {
// 建立變數用於儲存匹配的符號
var punctuators = char
// 判斷是否是箭頭函式的符號
if(char === '=' && input[current+1] === '>') {
punctuators += input[++current]
}
current++;
// 最後把資料更新到 tokens 中
tokens.push({
type: 'Punctuator',
value: punctuators
})
// 進入下一次迴圈
continue
}
// 處理空格: 如果是空格,則直接進入下一個迴圈
var WHITESPACE = /\s/
if (WHITESPACE.test(char)) {
current++
continue
}
// 處理數字: 檢查是否是數字
var NUMBERS = /[0-9]/
if (NUMBERS.test(char)) {
// 建立變數用於儲存匹配的數字
var number = ''
// // 迴圈遍歷接下來的字元,直到下一個字元不是數字為止
while (NUMBERS.test(char)) {
number += char
char = input[++current]
}
// 最後把資料更新到 tokens 中
tokens.push({
type: 'Numeric',
value: number
})
// 進入下一次迴圈
continue
}
// 處理字元: 檢查是否是字元
var LETTERS = /[a-z]/i
if (LETTERS.test(char)) {
var value = ''
// 用一個迴圈遍歷所有的字母,把它們存入 value 中。
while (LETTERS.test(char)) {
value += char
char = input[++current]
}
// 判斷當前字串是否是關鍵字
KEYWORD = /function|var|return|let|const|if|for/
if(KEYWORD.test(value)) {
// 標記關鍵字
tokens.push({
type: 'Keyword',
value: value
})
} else {
// 標記變數
tokens.push({
type: 'Identifier',
value: value
})
}
// 進入下一次迴圈
continue
}
// 最後如果我們沒有匹配上任何型別的 token,那麼我們丟擲一個錯誤。
throw new TypeError('I dont know what this character is: ' + char)
}
// 詞法分析器的最後我們返回 tokens 陣列。
return tokens
}
複製程式碼
語法分析
詞義分析後,接下來是語法分析, 接收詞義分析的 tokens
, 然後分析之間內部關係,最終生成抽象語法樹(Abstract Syntax Tree, 縮寫為AST)。
例如:
[ { "type": "Keyword", "value": "let" },
{ "type": "Identifier", "value": "a" },
{ "type": "Punctuator", "value": "=" },
{ "type": "Numeric", "value": "1" } ]
複製程式碼
語法分析後結果為:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "script"
}
複製程式碼
解析函式為
// 語法解析函式,接收 tokens 作為引數
function parser(tokens) {
// 記錄當前解析到詞的位置
var current = 0
// 通過遍歷來解析 token節點,定義 walk 函式
function walk() {
// 從當前 token 開始解析
var token = tokens[current]
// 獲取下一個節點的 token
var nextToken = tokens[current + 1]
// 對於不同型別的結點,對應的處理方法也不同
// 檢查是不是數字型別
if (token.type === 'Numeric') {
// 如果是,current 自增。
current++
// 然後我們會返回一個新的 AST 結點
return {
type: 'Literal',
value: Number(token.value),
row: token.value
}
}
// 檢查是不是變數型別
if (token.type === 'Identifier') {
// 如果是,current 自增。
current++;
// 然後我們會返回一個新的 AST 結點
return {
type: 'Identifier',
name: token.value,
};
}
// 檢查是不是運算子型別
if (token.type === 'Punctuator') {
// 如果是,current 自增。
current++;
// 判斷運算子型別,根據型別返回新的 AST 節點
if(/[\+\-\*/]/im.test(token.value))
return {
type: 'BinaryExpression',
operator: token.value,
}
if(/\=/.test(token.value))
return {
type: 'AssignmentExpression',
operator: token.value
}
}
// 檢查是不是關鍵字
if ( token.type === 'Keyword') {
var value = token.value
// 檢查是不是定義語句
if( value === 'var' || value === 'let' || value === 'const' ) {
current++;
// 獲取定義的變數
var variable = walk()
// 判斷是否是賦值符號
var equal = walk()
var rightVar
if(equal.operator === '=') {
// 獲取所賦予的值
rightVar = walk()
} else {
// 不是賦值符號,說明只是定義變數
rightVar = null
current--
}
// 定義宣告
var declaration = {
type: 'VariableDeclarator',
id: variable, // 定義的變數
init: rightVar // 賦予的值
}
// 定義要返回的節點
return {
type: 'VariableDeclaration',
declarations: [declaration],
kind: value,
};
}
}
// 遇到了一個型別未知的結點,就丟擲一個錯誤。
throw new TypeError(token.type);
}
// 現在,我們建立 AST,根結點是一個型別為 `Program` 的結點。
var ast = {
type: 'Program',
body: [],
sourceType: "script"
};
// 開始 walk 函式,把結點放入 ast.body 中。
while (current < tokens.length) {
ast.body.push(walk());
}
// 最後我們的語法分析器返回 AST
return ast;
}
複製程式碼
轉換
編譯器的下一步就是轉換。對 AST 抽象樹進行處理,可以在同語言間進行轉換,也可以轉換成一種全新的語言(參考 JSX 轉換)
轉換 AST 的時候,我們可以新增、移動、替代、刪除 AST抽象樹裡的節點。
轉化前:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "script"
}
複製程式碼
轉化後
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
複製程式碼
遍歷器
為了修改 AST 抽象樹,首先要對節點進行遍歷,採用深度遍歷的方法。遍歷函式:
// 所以我們定義一個遍歷器,它有兩個引數,AST 和 vistor
// visitor 定義轉化函式
function traverser(ast, visitor) {
// 遍歷樹中每個節點,呼叫 traverseNode
function traverseArray(array, parent) {
if(typeof array.forEach === 'function')
array.forEach(function(child) {
traverseNode(child, parent);
});
}
// 處理 ast 節點的函式, 使用 visitor 定義的轉換函式進行轉換
function traverseNode(node, parent) {
// 首先看看 visitor 中有沒有對應 type 的處理函式。
var method = visitor[node.type]
// 如果有,參入引數
if (method) {
method(node, parent)
}
// 下面對每一個不同型別的結點分開處理。
switch (node.type) {
// 從頂層的 Program 開始
case 'Program':
traverseArray(node.body, node)
break
// 如果不需要轉換,則直接退出
case 'VariableDeclaration':
case 'VariableDeclarator':
case 'AssignmentExpression':
case 'Identifier':
case 'Literal':
break
// 同樣,如果不能識別當前的結點,那麼就丟擲一個錯誤。
default:
throw new TypeError(node.type)
}
}
// 最後我們對 AST 呼叫 traverseNode,開始遍歷。注意 AST 並沒有父結點。
traverseNode(ast, null)
}
複製程式碼
轉換器
轉換器接用於遍歷過程中轉換資料,他接收之前構建好的 AST樹,然後把它和 visitor 傳遞進入我們的遍歷器中 ,最後得到一個新的 AST 抽象樹。
// 定義我們的轉換器函式,接收 AST 作為引數
function transformer(ast) {
// 建立新的 ast 抽象樹
var newAst = {
type: 'Program',
body: [],
sourceType: "script"
};
// 下面是個程式碼技巧,在父結點上定義一個屬性 context(上下文),之後,就可以把結點放入他們父結點的 context 中。
ast._context = newAst.body
// 我們把 AST 和 visitor 函式傳入遍歷器
traverser(ast, {
// 把 VariableDeclaration kind 屬性進行轉換
VariableDeclaration: function(node, parent) {
var variableDeclaration = {
type: 'VariableDeclaration',
declarations: node.declarations,
kind: "var"
};
// 把新的 VariableDeclaration 放入到 context 中。
parent._context.push(variableDeclaration)
}
});
// 最後返回建立好的新 AST。
return newAst
}
複製程式碼
程式碼生成
最後一步就是程式碼生成了,這個階段做的事情有時候會和轉換(transformation)重疊,但是程式碼生成最主要的部分還是根據 AST 來輸出程式碼。
程式碼生成器會遞迴地呼叫它自己,把 AST 中的每個結點列印到一個很大的字串中。
function codeGenerator(node) {
// 對於不同型別的結點分開處理
switch (node.type) {
// 如果是 Program 結點,那麼我們會遍歷它的 body 屬性中的每一個結點。
case 'Program':
return node.body.map(codeGenerator)
.join('\n')
// VariableDeclaration 結點
case 'VariableDeclaration':
return (
node.kind + ' ' + codeGenerator(node.declarations)
)
// VariableDeclarator 節點
case 'VariableDeclarator':
return (
codeGenerator(node.id) + ' = ' +
codeGenerator(node.init)
);
// 處理變數
case 'Identifier':
return node.name;
// 處理數值
case 'Literal':
return node.value;
// 如果我們不能識別這個結點,那麼丟擲一個錯誤。
default:
throw new TypeError(node.type);
}
}
複製程式碼
轉化前:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
複製程式碼
轉化後
var a = 1
複製程式碼
經過實踐,我們按照 Babel 原理實現了一個簡單的 JavaScript 編譯器。 現在可以接著擴充套件這些程式碼,實現自己的編譯器了!!!