实现一个简单的 JavaScript 编译器
栏目: JavaScript · 发布时间: 5年前
内容简介:Babel 是目前最常用的 JavaScript 编译器。能够编译 JS 代码,使得代码能够正常的在旧版本的浏览器上面运行;还能够转化 JSX 语法,使得 react 写的代码能够正常运行。下面,按照编译原理来实现一个简单的 JS 代码编译器,实现把 ES6 代码转化成 ES5,以充分了解 Babel 运行原理。转化后
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 编译器。 现在可以接着扩展这些代码,实现自己的编译器了!!!
参考资料
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- LLVM接受NVIDIA的“f18” Fortran编译器作为官方Fortran编译器
- 编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(四)结语
- Scala.js 0.6.29 发布,将 Scala 编译成 js 的编译器
- Go 编译器介绍
- Go 编译器介绍
- C++编译器优化
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
PHP+MySQL八大动态Web应用实战
Jono Bacom / 吴连河、李剑 / 电子工业出版社 / 2008-6 / 68.00元
本书详细介绍了利用PHP+MySQL开发常见类型Web应用程序的完整设计和编码技术,并对整体设计与关键代码给予了细致、深入的剖析。其内容注重实践,提供了翔实完整的实战代码;思路独树一帜,突破过多描述语言细节的窠臼;行文风趣幽默,轻松调侃中将项目的完整设计过程分析得一清二楚。书中的示例项目完整而实用,读者甚至无需任何改动即可在实际中加以运用。. 本书适合对PHP/MySQL有初步了解但缺乏完整......一起来看看 《PHP+MySQL八大动态Web应用实战》 这本书的介绍吧!