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 编译器。 如今能够接着扩展这些代码,实现本身的编译器了!!!