Babel 是目前最經常使用的 JavaScript 編譯器。可以編譯 JS 代碼,使得代碼可以正常的在舊版本的瀏覽器上面運行;還可以轉化 JSX 語法,使得 react 寫的代碼可以正常運行。html
下面,按照編譯原理來實現一個簡單的 JS 代碼編譯器,實現把 ES6 代碼轉化成 ES5,以充分了解 Babel 運行原理。node
let a = 1
複製代碼
轉化後react
var a = 1
複製代碼
編譯器的編譯原理大多分爲三個階段: 解析、轉換以及代碼生成git
編譯前,首先要對代碼進行解析,解析分爲兩個階段 詞義分析(Lexical Analysis) 和 語法分析(Syntactic Analysis)github
詞義分析是接收原始代碼進行分詞,最後生成 token。json
例如:
let a = 1
數組
詞義分析後結果爲:瀏覽器
[ { "type": "Keyword", "value": "let" },
{ "type": "Identifier", "value": "a" },
{ "type": "Punctuator", "value": "=" },
{ "type": "Numeric", "value": "1" } ]
複製代碼
詞義分析器函數爲:bash
// 解析代碼,最後返回 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 編譯器。 如今能夠接着擴展這些代碼,實現本身的編譯器了!!!