首先跟你们说一下我为何会有这个想法吧,由于最近在空闲时间学习Swift
和SwiftUI
的时候会常用到这种叫作尾闭包
的语法,就以为颇有趣。同时由于很早以前看过jamiebuilds的the-super-tiny-compiler,就想着能不能本身也实现一个相似的有趣好玩简单的编译器。因此就有了js-trailing-closure-toy-compiler这个项目,以及今天的这篇文章。javascript
对于不熟悉Swift
的同窗来讲,我先来解释一下什么是尾闭包
。简单来讲,就是若是一个函数的最后一个参数也是一个函数,那么咱们就可使用尾闭包的方式来传递最后一个函数。你们能够看下面的代码示例:html
// 例子中的 a 表示一个函数 // #1:简单版 // Swift 的方式 a(){ // 尾闭包的内容 } // JavaScript 的方式 a(() => {}) // #2:函数带参数 // Swift 的方式,这里先忽略参数的类型 a(1, 2, 3){ // 尾闭包的内容 } // JavaScript 的方式 a(1, 2, 3, () => {}) // #3:尾闭包带有参数 // Swift 的方式,这里先忽略参数的类型 a(1, 2, 3){ arg1, arg2 in // 尾闭包的内容 } // JavaScript 的方式 a(1, 2, 3, (arg1, arg2) => {})
若是关于Swift
的尾闭包还有什么疑问的话,你们能够看一下官方的文档Closures,里面解释的也很清楚。前端
我记得本身很早以前就看过the-super-tiny-compiler项目的源码,不过当时只是简单的看了一遍。就觉得本身掌握了里面的一些知识和原理。可是当我想实现我本身心中的这个想法的时候。却发现以前并无把这个项目里面实践的一些方法和技巧掌握好。因此我决定先好好的把这个项目的源码看懂,而后本身先实现一个跟原来的项目功能同样的样例以后才开始着手实现本身的小编译器。java
友情提示,接下来的文章内容比较长,建议收藏后再仔细阅读。node
从the-super-tiny-compiler咱们能够了解到,对于通常的编译器来讲。主要有四个步骤去完成编译的过程,这四个步骤分别是:git
token
)。好比if
,"hello"
,123
,let
,const
等等。token
转换成当前语言的抽象语法树,也就是AST
( Abstract Syntax Tree )。为何要这么作呢?由于这样处理以后,咱们就知道代码中语句的前后关系和层级关系。也知道运行的顺序,以及上下文等等相关的信息了。AST
转换成目标语言的AST
。为何要作这一步呢?对于相同功能的程序语句来讲,若是选择实现的语言不同,那它们的语法大几率也是不同的。这就致使了它们对应的抽象语法树也是不同的。因此咱们须要作一次转换,为了下一步生成目标语言的代码作好准备。上面的步骤就是编译器的大概工做流程了,可是仅仅知道这些流程仍是不够的,还须要咱们亲自动手实践一下。若是看到这里你有兴趣的话,能够点击这里JavaScript Trailing Closure Toy Compiler先体验一下最后实现的效果。若是你对具体的实现过程感兴趣的话,能够继续下面的阅读,相信看过以后你会有很大的收获的,也许会想要本身也实现一个有趣的编译器呢。github
token
首先咱们须要明白为何要把字符串转换为一个个的token
,由于若是不作转换,咱们就不知道这段程序要表示的是什么意思,由于token
是理解一段程序的必要条件。swift
这就比如console.log("hello world!")
这个语句来讲,咱们一眼就知道它是干吗的,可是咱们是怎么思考的呢?是否是首先是console
咱们知道是console
对象,而后是.
咱们知道是获取对象的属性操做符,再而后是log
方法,而后方法的调用须要(
左括号做为开始,而后是hello world!
字符串为参数,而后遇到了后面的)
右括号表示结束。segmentfault
因此把字符串转换成token
就是为了让咱们知道这段程序要表示的是什么意思。由于根据每个token
的值,以及token
所处的位置,咱们能够准确知道这个token
表示的是什么,它有什么做用。设计模式
那对于咱们这个编译器来讲,第一步须要把咱们所须要的token
作一个划分,那么根据上面的代码示例。咱们能够知道,咱们须要的token
的类型有这么几种:
1
,66
等。"hello"
等。a
,在咱们这个编译器的环境下,通常表示函数名或者变量名。(
和)
,在这里用来表示函数的调用。{
和}
,在这里用来表示函数体。,
,用来分割参数。
,用来区分不一样的token
。由于咱们这个编译器暂时只专一于咱们想要的尾闭包的实现,因此暂时只须要关注上面这些token
的类型就能够了。
这一步其实比较简单,就是按照咱们的需求,循环读取token
,代码部分以下所示:
// 将字符串解析为Tokens const tokenizer = (input) => { // 简单的正则 const numReg = /\d/; const idReg = /[a-z]/i; const spaceReg = /\s/; // Tokens 数组 const tokens = []; // 判断 input 的长度 const len = input.length; if (len > 0) { let cur = 0; while(cur < len) { let curChar = input[cur]; // 判断是不是数字 if (numReg.test(curChar)) { let num = ''; while(numReg.test(curChar) && curChar) { num += curChar; curChar = input[++cur]; } tokens.push({ type: 'NumericLiteral', value: num }); continue; } // 判断是不是标识符 if (idReg.test(curChar)) { let idVal = ''; while(idReg.test(curChar) && curChar) { idVal += curChar; curChar = input[++cur]; } // 判断是不是 in 关键字 if (idVal === 'in') { tokens.push({ type: 'InKeyword', value: idVal }); } else { tokens.push({ type: 'Identifier', value: idVal }); } continue; } // 判断是不是字符串 if (curChar === '"') { let strVal = ''; curChar = input[++cur]; while(curChar !== '"') { strVal += curChar; curChar = input[++cur]; } tokens.push({ type: 'StringLiteral', value: strVal }); // 须要处理字符串的最后一个双引号 cur++; continue; } // 判断是不是左括号 if (curChar === '(') { tokens.push({ type: 'ParenLeft', value: '(' }); cur++; continue; } // 判断是不是右括号 if (curChar === ')') { tokens.push({ type: 'ParenRight', value: ')' }); cur++; continue; } // 判断是不是左花括号 if (curChar === '{') { tokens.push({ type: 'BraceLeft', value: '{' }); cur++; continue; } // 判断是不是右花括号 if (curChar === '}') { tokens.push({ type: 'BraceRight', value: '}' }); cur++; continue; } // 判断是不是逗号 if (curChar === ',') { tokens.push({ type: 'Comma', value: ',' }); cur++; continue; } // 判断是不是空白符号 if (spaceReg.test(curChar)) { cur++; continue; } throw new Error(`${curChar} is not a good character`); } } console.log(tokens, tokens.length); return tokens; };
上面的代码虽然不是很复杂,可是有一些须要注意的点,若是不细心很容易出错或者进入一个死循环。下面是我以为一些容易出现问题的地方:
while
循环,每次循环开始时会首先获取当前下标对应的字符。之因此没有使用for
循环是由于这里关于当前字符的下标cur
是由里面的判断来推动的,使用while
更方便一些。"
须要跳过,不计入字符串的值里面。遇到空白符须要跳过。这个过程技术难度不大,须要多一点耐心。实现完成以后,咱们能够测试一下:
tokenizer(`a(1){}`)
能够看到输出的结果以下:
(6) [{…}, {…}, {…}, {…}, {…}, {…}] 0: {type: "Identifier", value: "a"} 1: {type: "ParenLeft", value: "("} 2: {type: "NumericLiteral", value: "1"} 3: {type: "ParenRight", value: ")"} 4: {type: "BraceLeft", value: "{"} 5: {type: "BraceRight", value: "}"}
能够看到输出的结果是咱们想要的结果,到这里咱们已经成功了25%了。接下来就是把获得的token
数组转换为AST
抽象语法树。
token
数组转换为AST
抽象语法树接下来的步骤就是把token
数组转换为AST(抽象语法树)
了,进行了上一个步骤以后,咱们把代码字符串,转变为一个个有意义的token
。当咱们获得了这些token
以后,就能够根据每个token
表示的意义进而推导出整个抽象语法树。
好比咱们遇到了{
,咱们就知道在遇到下一个}
为止,这中间的全部的token
表示的是一个函数的函数体(暂时不考虑其它状况)。
下图所示的token
示例:
表示的程序语句应该是:
a(1) { // block };
那么它所对应的抽象语法树应该是这个样子的:
{ "type": "Program", "body": [ { "type": "CallExpression", "value": "a", "params": [ { "type": "NumericLiteral", "value": "1", "parentType": "ARGUMENTS_PARENT_TYPE" } ], "hasTrailingBlock": true, "trailingBlockParams": [], "trailingBody": [] } ] }
咱们能够简单的看一下上面的抽象语法树,首先最外层的类型是Program
,而后body
里面的内容就表示咱们的代码内容。在这里咱们的body
数组只有一个元素,表示的是CallExpression
,也就是一个函数调用。
这个CallExpression
的函数名字是a
,而后函数第一个参数类型值是NumericLiteral
,数值是1
。这个参数的父节点类型是ARGUMENTS_PARENT_TYPE
,下面还会对这个属性进行解释。而后这个CallExpression
的hasTrailingBlock
值为true
,表示这是一个尾闭包函数调用。而后trailingBlockParams
表示尾闭包没有参数,trailingBody
表示尾闭包里面的内容为空。
上面只是一个简单的解释,详细的代码部分以下所示:
// 将 Tokens 转换为 AST const parser = (tokens) => { const ast = { type: 'Program', body: [] }; let cur = 0; const walk = () => { let token = tokens[cur]; // 是数字直接返回 if (token.type === 'NumericLiteral') { cur++; return { type: 'NumericLiteral', value: token.value }; } // 是字符串直接返回 if (token.type === 'StringLiteral') { cur++; return { type: 'StringLiteral', value: token.value }; } // 是逗号直接返回 if (token.type === 'Comma') { cur++; return; } // 若是是标识符,在这里咱们只有函数的调用,因此须要判断函数有没有其它的参数 if (token.type === 'Identifier') { const callExp = { type: 'CallExpression', value: token.value, params: [], hasTrailingBlock: false, trailingBlockParams: [], trailingBody: [] }; // 指定节点对应的父节点的类型,方便后面的判断 const specifyParentNodeType = () => { // 过滤逗号 callExp.params = callExp.params.filter(p => p); callExp.trailingBlockParams = callExp.trailingBlockParams.filter(p => p); callExp.trailingBody = callExp.trailingBody.filter(p => p); callExp.params.forEach((node) => { node.parentType = ARGUMENTS_PARENT_TYPE; }); callExp.trailingBlockParams.forEach((node) => { node.parentType = ARGUMENTS_PARENT_TYPE; }); callExp.trailingBody.forEach((node) => { node.parentType = BLOCK_PARENT_TYPE; }); }; const handleBraceBlock = () => { callExp.hasTrailingBlock = true; // 收集闭包函数的参数 token = tokens[++cur]; const params = []; const blockBody = []; let isParamsCollected = false; while(token.type !== 'BraceRight') { if (token.type === 'InKeyword') { callExp.trailingBlockParams = params; isParamsCollected = true; token = tokens[++cur]; } else { if (!isParamsCollected) { params.push(walk()); token = tokens[cur]; } else { // 处理花括号里面的数据 blockBody.push(walk()); token = tokens[cur]; } } } // 若是 isParamsCollected 到这里仍是 false,说明花括号里面没有参数 if (!isParamsCollected) { // 若是没有参数 收集的就不是参数了 callExp.trailingBody = params; } else { callExp.trailingBody = blockBody; } // 处理右边的花括号 cur++; }; // 判断后面紧接着的 token 是 `(` 仍是 `{` // 须要判断当前的 token 是函数调用仍是参数 const next = tokens[cur + 1]; if (next.type === 'ParenLeft' || next.type === 'BraceLeft') { token = tokens[++cur]; if (token.type === 'ParenLeft') { // 须要收集函数的参数 // 须要判断下一个 token 是不是 `)` token = tokens[++cur]; while(token.type !== 'ParenRight') { callExp.params.push(walk()); token = tokens[cur]; } // 处理右边的圆括号 cur++; // 获取 `)` 后面的 token token = tokens[cur]; // 处理后面的尾部闭包;须要判断 token 是否存在 考虑`func()` if (token && token.type === 'BraceLeft') { handleBraceBlock(); } } else { handleBraceBlock(); } // 指定节点对应的父节点的类型 specifyParentNodeType(); return callExp; } else { cur++; return { type: 'Identifier', value: token.value }; } } throw new Error(`this ${token} is not a good token`); }; while (cur < tokens.length) { ast.body.push(walk()); } console.log(ast); return ast; };
为了方便你们理解,我把一些关键的地方都添加了一些注释。下面再次对上面的代码作一些简单的解释。
首先咱们须要对tokens
数组进行遍历,咱们首先定义了抽象语法树的最外层的结构是:
const ast = { type: 'Program', body: [] };
这样定义是为了后续的节点对象可以按照必定的规则添加到咱们的抽象语法树上。
而后咱们定义了一个walk
函数用来对tokens
数组中的元素进行遍历。对于walk
函数来讲,若是直接遇到数字
,字符串
,逗号
的话都是直接返回的。当遇到的token
是一个标识符的话,须要判断的状况比较多。
对于一个标识符来讲,在咱们这种情境下有两种处理:
token
既不是表示(
的,也不是表示{
的。另外一种状况表示的是一个函数的调用,对于函数的调用来讲,咱们须要考虑如下这几种状况:
a{}
;a()
或者a(1)
;a{}
,a(){}
,a(1){}
等等。对于有尾闭包的状况还须要考虑尾部闭包有没有参数,好比a(1){b, c in }
。接下来主要对token
是标识符类型的处理作一个简单的解释,若是判断token
的类型是标识符的话,咱们会先定义一个CallExpression
类型的对象callExp
,这个对象就是用来表示咱们函数调用的语法树对象。这个对象有如下几个属性:
type
:表示节点的类型value
:表示节点的名称,这里表示函数名params
:表示函数调用的参数hasTrailingBlock
:表示当前函数调用是否包含尾闭包trailingBlockParams
:表示尾闭包是否含有参数trailingBody
:尾闭包里面的内容接下来判断标识符后面的token
类型是什么,若是是函数的调用的话,当前token
后面的token
必须是(
或者是{
。若是不是的话,咱们直接返回这个标识符。
若是是函数的调用,咱们须要作两个事情,一个是收集函数调用的参数,一个是判断函数调用后面是否是含有尾闭包。对于函数参数的收集比较简单,首先判断当前token
后面的token
是否是表示的是(
,若是是的话,开始收集参数,直到遇到下一个token
的类型是)
表示参数收集结束。还要注意的一点是,由于参数有多是一个函数,因此咱们须要在收集参数的时候再次调用walk
函数,来帮助咱们递归的进行参数的处理。
接下来就是判断函数调用后面是否含有尾闭包,对于尾闭包的判断有两种状况须要考虑:一种就是函数的调用含有有参数,在参数的后面含有尾闭包;另外一种是函数的调用没有参数,直接就是一个尾闭包。因此咱们须要对这两种状况都作一下处理。
既然有两个地方都要进行是不是尾闭包的判断,咱们能够把这部分的逻辑抽离到handleBraceBlock
函数中,这个函数就是帮助咱们来进行尾闭包的处理。接下来来解释一下尾闭包是如何进行处理的。
若是咱们判断下一个token
是{
那么说明咱们须要进行尾闭包的处理了,咱们首先把callExp
对象的hasTrailingBlock
属性的值设置为true
;而后须要判断尾闭包是否含有参数,而且须要处理尾闭包的内部内容。
如何收集尾闭包的参数呢?咱们须要判断在尾闭包里面是否含有in
关键字,若是含有in
关键字,那就说明尾闭包里面含有参数,若是没有就表示尾闭包里不含有参数,只须要处理尾闭包内部的内容便可。
又由于咱们刚开始不知道尾闭包中是否含有in
关键字,因此咱们一开始收集的内容多是尾闭包里面的内容,也有多是参数;因此当在遇到}
尾闭包的结束token
以后,这期间若是没有in
关键字,那说明咱们收集到的都是尾闭包的内容。
不管是收集尾闭包的参数仍是内容,咱们都须要使用walk
函数来进行递归操做,由于参数和内容均可能不是基本的数值类型值(为了简化操做,咱们这里对尾闭包的参数也使用walk
来进行递归操做)。
在返回callExp
对象以前,咱们须要使用specifyParentNodeType
帮助函数额外的作一下处理。第一个处理是去掉表示,
的token
,另外一个操做就是须要给callExp
对象的params
,trailingBlockParams
和trailingBody
属性中的节点指定一下父节点的类型,对于params
和trailingBlockParams
来讲,它们的父节点类型都是ARGUMENTS_PARENT_TYPE
类型的;对于trailingBody
来讲,它的父节点类型是BLOCK_PARENT_TYPE
类型的。这样的处理方便咱们进行下一步的操做。在进行下面步骤讲解的时候咱们会再次对其进行说明。
接下来就是把咱们获取到的原始AST
转换为目标语言的AST
,那么咱们为何要作这一步处理呢?这是由于一样的编码逻辑,在不一样的宿主语言的表现是不同的。因此咱们要把原始的AST
转换成咱们目标语言的AST
。
那咱们怎么进行操做呢?原始的AST
是一个树形的结构,咱们须要对这个树形的结构进行遍历;遍历须要使用深度优先的遍历,由于对于一个嵌套的结构来讲,只有将里面的内容肯定了以后,外面的内容才可以随之肯定。
这里对树形结构的遍历咱们会用到一种设计模式,那就是访问者模式。咱们须要一个访问者对象对咱们的树形对象进行深度优先的遍历,这个访问者对象有针对不一样类型节点的处理函数,当遇到一个节点的时候,咱们就会根据当前节点的类型,从访问者对象身上获取相应的处理函数对这个节点进行处理。
咱们首先看一下如何对原始的树形结构进行遍历,对于原来的树形结构来讲,每个节点要么是一个具体类型的对象,要么是一个数组。因此咱们要对这两种状况分别进行处理。咱们首先肯定如何进行树形结构的遍历,这部分的代码以下所示:
// 遍历节点 const traverser = (ast, visitor) => { const traverseNode = (node, parent) => { const method = visitor[node.type]; if (method && method.enter) { method.enter(node, parent); } const t = node.type; switch (t) { case 'Program': traverseArr(node.body, node); break; case 'CallExpression': // 处理 ArrowFunctionExpression // TODO 考虑body 里面存在尾部闭包 if (node.hasTrailingBlock) { node.params.push({ type: 'ArrowFunctionExpression', parentType: ARGUMENTS_PARENT_TYPE, params: node.trailingBlockParams, body: node.trailingBody }); traverseArr(node.params, node); } else { traverseArr(node.params, node); } break; case 'ArrowFunctionExpression': traverseArr(node.params, node); traverseArr(node.body, node); break; case 'Identifier': case 'NumericLiteral': case 'StringLiteral': break; default: throw new Error(`this type ${t} is not a good type`); } if (method && method.exit) { method.exit(node, parent); } }; const traverseArr = (arr, parent) => { arr.forEach((node) => { traverseNode(node, parent); }); }; traverseNode(ast, null); };
我来简单解释一下这个traverser
函数,这个函数内部定义了两个函数,一个是traverseNode
,一个是traverseArr
。traverseArr
函数的做用是,若是当前的节点是一个数组的话,咱们须要对数组里面的每个节点分别进行处理。
节点的主要处理逻辑都在traverseNode
里面,咱们来看一下这个函数都作了哪些事情?首先根据节点的类型,从visitor
对象上获取对应节点的处理方法。而后对接点类型进行判断,若是节点的类型是基本类型的话,就不作处理;若是节点的类型是ArrowFunctionExpression
箭头函数的话,须要依次遍历这个节点的params
和body
属性。若是节点的类型是CallExpression
的话,表示当前的节点是一个函数调用节点,那么咱们就须要判断这个函数调用是否包含尾闭包,若是包含尾闭包的话,那就说明咱们原来的函数调用须要额外添加一个参数,这个参数是一个箭头函数。因此会有下面这样一段代码进行判断:
// ... if (node.hasTrailingBlock) { node.params.push({ type: 'ArrowFunctionExpression', parentType: ARGUMENTS_PARENT_TYPE, params: node.trailingBlockParams, body: node.trailingBody }); traverseArr(node.params, node); } else { traverseArr(node.params, node); } // ...
而后就是对这个CallExpression
节点的params
属性进行遍历。当函数的调用包含尾闭包的时候,咱们往节点的params
属性里添加了一个类型是ArrowFunctionExpression
的对象,并且这个对象的parentType
的值是ARGUMENTS_PARENT_TYPE
,由于这样咱们就知道这个对象的父节点类型,方便咱们下面进行语法树转换时使用。
再接下来就是定义访问者对象上面不一样节点类型的处理方法了,具体的代码以下:
const transformer = (ast) => { const newAst = { type: 'Program', body: [] }; ast._container = newAst.body; const getNodeContainer = (node, parent) => { const parentType = node.parentType; if (parentType) { if (parentType === BLOCK_PARENT_TYPE) { return parent._bodyContainer; } if (parentType === ARGUMENTS_PARENT_TYPE) { return parent._argumentsContainer; } } else { return parent._container; } }; traverser(ast, { NumericLiteral: { enter: (node, parent) => { getNodeContainer(node, parent).push({ type: 'NumericLiteral', value: node.value }); } }, StringLiteral: { enter: (node, parent) => { getNodeContainer(node, parent).push({ type: 'StringLiteral', value: node.value }); } }, Identifier: { enter: (node, parent) => { getNodeContainer(node, parent).push({ type: 'Identifier', name: node.value }); } }, CallExpression: { enter: (node, parent) => { // TODO 优化一下 const callExp = { type: 'CallExpression', callee: { type: 'Identifier', name: node.value }, arguments: [], blockBody: [] }; // 给参数添加 _container node._argumentsContainer = callExp.arguments; node._bodyContainer = callExp.blockBody; getNodeContainer(node, parent).push(callExp); } }, ArrowFunctionExpression: { enter: (node, parent) => { // TODO 优化一下 const arrowFunc = { type: 'ArrowFunctionExpression', arguments: [], blockBody: [] }; // 给参数添加 _container node._argumentsContainer = arrowFunc.arguments; node._bodyContainer = arrowFunc.blockBody; getNodeContainer(node, parent).push(arrowFunc); } } }); console.log(newAst); return newAst; };
咱们首先定义了新的AST
的外层属性,而后是ast._container = newAst.body
,这个操做的做用是将旧的AST
和新的AST
最外层进行关联,由于咱们遍历的是旧的AST
。这样咱们就能够经过_container
属性指向新的AST
。这样咱们向_container
里面添加元素的时候,实际上就是在新的AST
上添加对应的节点。这样处理对咱们来讲相对比较简单一点。
而后就是getNodeContainer
函数,这个函数的做用就是获取当前节点的父节点的_container
属性,若是当前节点的parentType
属性不为空,那说明当前节点的父节点表示的多是函数调用的参数,也有多是尾闭包里面的内容。这时能够根据node.parentType
的类型进行判断。若是当前节点的parentType
属性为空,那就说明当前节点的父节点的_container
属性就是父节点的_container
属性。
接下来就是visitor
对象上面不一样节点类型的处理方法了,对于基本类型仍是直接返回对应的节点就能够了。若是是CallExpression
和ArrowFunctionExpression
类型的话,就须要一些额外的处理了。
首先对于ArrowFunctionExpression
类型节点来讲,首先声明了一个arrowFunc
对象,而后将对应节点的_argumentsContainer
属性指向arrowFunc
对象的arguments
属性;将节点的_bodyContainer
属性指向arrowFunc
对象的blockBody
属性。而后获取当前节点的父节点的_container
属性,最后将arrowFunc
添加到这个属性上。对于节点类型是CallExpression
的节点的处理跟上面的相似,只不过定义的对象多了一个callee
属性,代表函数调用的函数名称。
到此为止将旧的AST
转换为新的AST
就完成了。
这一步就比较简单了,根据节点的类型拼接对应类型的代码就能够了;详细的代码以下所示:
const codeGenerator = (node) => { const type = node.type; switch (type) { case 'Program': return node.body.map(codeGenerator).join(';\n'); case 'Identifier': return node.name; case 'NumericLiteral': return node.value; case 'StringLiteral': return `"${node.value}"`; case 'CallExpression': return `${codeGenerator(node.callee)}(${node.arguments.map(codeGenerator).join(', ')})`; case 'ArrowFunctionExpression': return `(${node.arguments.map(codeGenerator).join(', ')}) => {${node.blockBody.map(codeGenerator).join(';')}}`; default: throw new Error(`this type ${type} is not a good type`); } };
须要注意的可能就是对于CallExpression
和ArrowFunctionExpression
节点的处理了,对于CallExpression
须要添加函数的名称,而后接下来就是函数调用的参数了。对于ArrowFunctionExpression
来讲,须要处理箭头函数的参数以及函数体的内容。相比上面的三个步骤来讲,这个步骤仍是相对比较简单的。
接下来就是将这四个步骤组合一下,这个简单的编译器就算完成了。具体的代码以下所示:
// 组装 const compiler = (input) => { const tokens = tokenizer(input); const ast = parser(tokens); const newAst = transformer(ast); return codeGenerator(newAst); }; // 导出对应的模块 module.exports = { tokenizer, parser, transformer, codeGenerator, compiler };
若是你有耐心看完的话,你会发现完成一个简单的编译器其实也没有很复杂。咱们须要把这四个过程要作什么理清楚,而后注意一些特殊的地方须要作特殊的处理,还有一个就是须要一点耐心了。
固然咱们这个版本的实现只是简单的完成了咱们想要的那部分功能,实际上真正的编译器要考虑的东西是很是多的。上面这个版本的代码有不少地方也不是很规范,当初实现的时候先考虑如何实现,细节和可维护性没有考虑太多。若是你有什么好的想法,或者发现了什么错误欢迎给这个小项目提Issues或者Pull Request,让这个小项目变得更好一点。也欢迎你们在文章下面留言,看看是否是能碰撞出什么新的思路与想法。
一些同窗可能会说,学习这种东西有什么做用?其实用途有不少,首先咱们如今前端的构建基本上离不开Babel对JavaScript
新特性的支持,而Babel
的做用其实就是一个编译器的做用,把咱们语言新的特性转换成目前的浏览器能够支持的一些语法,让咱们能够方便的使用新的语法,也减轻了前端开发的一些负担。
另外一方面你若是知道这些原理,你不只能够很容易看懂一些Babel
的语法转换插件的源代码,你还能够本身亲自动手实现一个简单的语法转换器或者一些有意思的插件。这会让你的前端能力有一个大的提高。
时间过得好快,距离上次发布文章已通过去两个月了😂,这篇文章也是过完年后的第一篇文章,但愿之后还可以持续的输出一些高质量的文章。固然以前的设计模式大冒险系列还会持续更新,也欢迎你们继续保持关注。
今天的文章到这里就结束了,若是你对这篇文章有什么意见和建议,欢迎在文章下面留言,或者在这里提出来。也欢迎你们关注个人公众号关山不难越,若是你以为这篇文章写的不错,或者对你有所帮助,那就点赞分享一下吧~
参考: