点击关注本公众号获取文档最新更新,并能够领取配套于本指南的 《前端面试手册》 以及最标准的简历模板.javascript
Babel 是现代 JavaScript 语法转换器,几乎在任何现代前端项目中都能看到他的身影,其背后的原理对于大部分开发者还属于黑盒,不过 Babel 做为一个工具真的有了解背后原理的必要吗?css
若是只是 Babel 可能真没有必要,问题是其背后的原理在咱们开发中应用过于普遍了,包括不限于: eslint jshint stylelint css-in-js prettier jsx vue-template uglify-js postcss less 等等等等,从模板到代码检测,从混淆压缩到代码转换,甚至编辑器的代码高亮都与之息息相关.前端
若是有兴趣就能够搞一些黑魔法: 前端工程师能够用编译原理作什么?vue
Babel 大概分为三大部分:java
咱们主要经过打造一个微型 babel 来了解 babel 的基本原理,这个微型 babel 的功能很单一也很鸡肋,可是依然有400行代码,其实现细节与 babel 并不相同,由于咱们省去了不少额外的验证和信息解析,由于单单一个兼容现代 JavaScript 语法的 parser 就须要5000行代码,并不利于咱们快速了解 babel 的基本实现,因此这个微型 babel能够说比较鸡肋(由于除了展现以外没啥用处),可是比较完整展现了 babel 的基本原理,你能够以此做为入门,在入门以后若是仍有兴趣,能够阅读:node
代码解析,也就是咱们常说的 Parser, 用于将一段代码(文本)解析成一个数据结构.git
例如这段 es6的代码程序员
const add = (a, b) => a + b
咱们用 babel 解析后即是这种形式:es6
{ "type": "File", "start": 0, "end": 27, "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 27 } }, "program": { "type": "Program", "start": 0, "end": 27, "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 27 } }, "sourceType": "module", "body": [ { "type": "VariableDeclaration", "start": 0, "end": 27, "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 27 } }, "declarations": [ { "type": "VariableDeclarator", "start": 6, "end": 27, "loc": { "start": { "line": 1, "column": 6 }, "end": { "line": 1, "column": 27 } }, "id": { "type": "Identifier", "start": 6, "end": 9, "loc": { "start": { "line": 1, "column": 6 }, "end": { "line": 1, "column": 9 }, "identifierName": "add" }, "name": "add" }, "init": { "type": "ArrowFunctionExpression", "start": 12, "end": 27, "loc": { "start": { "line": 1, "column": 12 }, "end": { "line": 1, "column": 27 } }, "id": null, "generator": false, "expression": true, "async": false, "params": [ { "type": "Identifier", "start": 13, "end": 14, "loc": { "start": { "line": 1, "column": 13 }, "end": { "line": 1, "column": 14 }, "identifierName": "a" }, "name": "a" }, { "type": "Identifier", "start": 16, "end": 17, "loc": { "start": { "line": 1, "column": 16 }, "end": { "line": 1, "column": 17 }, "identifierName": "b" }, "name": "b" } ], "body": { "type": "BinaryExpression", "start": 22, "end": 27, "loc": { "start": { "line": 1, "column": 22 }, "end": { "line": 1, "column": 27 } }, "left": { "type": "Identifier", "start": 22, "end": 23, "loc": { "start": { "line": 1, "column": 22 }, "end": { "line": 1, "column": 23 }, "identifierName": "a" }, "name": "a" }, "operator": "+", "right": { "type": "Identifier", "start": 26, "end": 27, "loc": { "start": { "line": 1, "column": 26 }, "end": { "line": 1, "column": 27 }, "identifierName": "b" }, "name": "b" } } } } ], "kind": "const" } ], "directives": [] } }
咱们以解析上面的 es6箭头函数为目标,来写一个简单的 parser.github
文本 ---> AST 的过程当中有两个关键步骤:
要作词法分析,首先咱们须要明白在 JavaScript 中哪些属于语法单元
在咱们 parser 的过程当中,应该换一个角度看待代码,咱们平时工做用的代码.本质是就是字符串或者一段文本,它没有任何意义,是 JavaScript 引擎赋予了它意义,因此咱们在解析过程当中代码只是一段字符串.
仍然如下面代码为例
const add = (a, b) => a + b
咱们指望的结果是相似这样的
[ { type: "identifier", value: "const" }, { type: "whitespace", value: " " }, ... ]
那么咱们如今开始打造一个Tokenizer(词法分析器)
// 词法分析器,接收字符串返回token数组 export const tokenizer = (code) => { // 储存 token 的数组 const tokens = []; // 指针 let current = 0; while (current < code.length) { // 获取指针指向的字符 const char = code[current]; // 咱们先处理单字符的语法单元 相似于`;` `(` `)`等等这种 if (char === '(' || char === ')') { tokens.push({ type: 'parens', value: char, }); current ++; continue; } // 咱们接着处理标识符,标识符通常为以字母、_、$开头的连续字符 if (/[a-zA-Z\$\_]/.test(char)) { let value = ''; value += char; current ++; // 若是是连续字那么将其拼接在一块儿,随后指针后移 while (/[a-zA-Z0-9\$\_]/.test(code[current]) && current < code.length) { value += code[current]; current ++; } tokens.push({ type: 'identifier', value, }); continue; } // 处理空白字符 if (/\s/.test(char)) { let value = ''; value += char; current ++; //道理同上 while (/\s]/.test(code[current]) && current < code.length) { value += code[current]; current ++; } tokens.push({ type: 'whitespace', value, }); continue; } // 处理逗号分隔符 if (/,/.test(char)) { tokens.push({ type: ',', value: ',', }); current ++; continue; } // 处理运算符 if (/=|\+|>/.test(char)) { let value = ''; value += char; current ++; while (/=|\+|>/.test(code[current])) { value += code[current]; current ++; } // 当 = 后面有 > 时为箭头函数而非运算符 if (value === '=>') { tokens.push({ type: 'ArrowFunctionExpression', value, }); continue; } tokens.push({ type: 'operator', value, }); continue; } // 若是碰到咱们词法分析器之外的字符,则报错 throw new TypeError('I dont know what this character is: ' + char); } return tokens; };
那么咱们基本的词法分析器就打造完成,由于只针对这一个es6函数,因此没有作额外的工做(额外的工做量会很是庞大).
const result = tokenizer('const add = (a, b) => a + b') console.log(result); /** [ { type: 'identifier', value: 'const' }, { type: 'whitespace', value: ' ' }, { type: 'identifier', value: 'add' }, { type: 'whitespace', value: ' ' }, { type: 'operator', value: '=' }, { type: 'whitespace', value: ' ' }, { type: 'parens', value: '(' }, { type: 'identifier', value: 'a' }, { type: ',', value: ',' }, { type: 'whitespace', value: ' ' }, { type: 'identifier', value: 'b' }, { type: 'parens', value: ')' }, { type: 'whitespace', value: ' ' }, { type: 'ArrowFunctionExpression', value: '=>' }, { type: 'whitespace', value: ' ' }, { type: 'identifier', value: 'a' }, { type: 'whitespace', value: ' ' }, { type: 'operator', value: '+' }, { type: 'whitespace', value: ' ' }, { type: 'identifier', value: 'b' } ] **/
语法分析要比词法分析复杂得多,由于咱们接下来的是示意代码,因此作了不少“武断”的判断来省略代码,即便这样也是整个微型 babel 中代码量最多的.
语法分析之因此复杂,是由于要分析各类语法的可能性,须要开发者根据token流(上一节咱们生成的 token 数组)提供的信息来分析出代码之间的逻辑关系,只有通过词法分析 token 流才能成为有结构的抽象语法树.
作语法分析最好依照标准,大多数 JavaScript Parser 都遵循estree规范
因为标准内容不少,感兴趣的能够去阅读,咱们目前只介绍几个比较重要的标准:
语句(Statements): 语句是 JavaScript 中很是常见的语法,咱们常见的循环、if 判断、异常处理语句、with 语句等等都属于语句
// 典型的for 循环语句 for (var i = 0; i < 7; i++) { console.log(i); }
表达式(Expressions): 表达式是一组代码的集合,它返回一个值,表达式是另外一个十分常见的语法,函数表达式就是一种典型的表达式,若是你不理解什么是表达式, MDN上有很详细的解释.
// 函数表达式 var add = function(a, b) { return a + b }
声明(Declarations): 声明分为变量声明和函数声明,表达式(Expressions)中的函数表达式的例子用声明的写法就是下面这样.
// 函数声明 function add(a, b) { return a + b }
你可能有点糊涂,为了理清其中的关系,咱们就下面的代码为例来解读
// 函数表达式 var add = function(a, b) { return a + b }
首先这段代码的总体本质是是一个变量声明(VariableDeclarator):
而变量被声明为一个函数表达式(FunctionExpression):
函数表达式中的大括号在内的为块状语句(BlockStatement):
块状语句内 return 的部分是返回语句(ReturnStatement):
而 return 的实际上是一个二元运算符或者叫二元表达式(BinaryExpression):
上面提到的这些有些属于表达式,有些属于声明也有些属于语句,固然还有更多咱们没提到的,它们被语法分析以后被叫作AST(抽象语法树).
咱们作语法分析的时候思路也是相似的,要分析哪一层的 token 到底属于表达式或者说语句,若是是语句那么是块状语句(BlockStatement)仍是Loops,若是是 Loops 那么属于while 循环(WhileStatement)仍是for 循环(ForStatement)等等,其中甚至不免要考虑做用域的问题,所以语法分析的复杂也体如今此.
const parser = tokens => { // 声明一个全时指针,它会一直存在 let current = -1; // 声明一个暂存栈,用于存放临时指针 const tem = []; // 指针指向的当前token let token = tokens[current]; const parseDeclarations = () => { // 暂存当前指针 setTem(); // 指针后移 next(); // 若是字符为'const'可见是一个声明 if (token.type === 'identifier' && token.value === 'const') { const declarations = { type: 'VariableDeclaration', kind: token.value }; next(); // const 后面要跟变量的,若是不是则报错 if (token.type !== 'identifier') { throw new Error('Expected Variable after const'); } // 咱们获取到了变量名称 declarations.identifierName = token.value; next(); // 若是跟着 '=' 那么后面应该是个表达式或者常量之类的,额外判断的代码就忽略了,直接解析函数表达式 if (token.type === 'operator' && token.value === '=') { declarations.init = parseFunctionExpression(); } return declarations; } }; const parseFunctionExpression = () => { next(); let init; // 若是 '=' 后面跟着括号或者字符那基本判断是一个表达式 if ( (token.type === 'parens' && token.value === '(') || token.type === 'identifier' ) { setTem(); next(); while (token.type === 'identifier' || token.type === ',') { next(); } // 若是括号后跟着箭头,那么判断是箭头函数表达式 if (token.type === 'parens' && token.value === ')') { next(); if (token.type === 'ArrowFunctionExpression') { init = { type: 'ArrowFunctionExpression', params: [], body: {} }; backTem(); // 解析箭头函数的参数 init.params = parseParams(); // 解析箭头函数的函数主体 init.body = parseExpression(); } else { backTem(); } } } return init; }; const parseParams = () => { const params = []; if (token.type === 'parens' && token.value === '(') { next(); while (token.type !== 'parens' && token.value !== ')') { if (token.type === 'identifier') { params.push({ type: token.type, identifierName: token.value }); } next(); } } return params; }; const parseExpression = () => { next(); let body; while (token.type === 'ArrowFunctionExpression') { next(); } // 若是以(开头或者变量开头说明不是 BlockStatement,咱们以二元表达式来解析 if (token.type === 'identifier') { body = { type: 'BinaryExpression', left: { type: 'identifier', identifierName: token.value }, operator: '', right: { type: '', identifierName: '' } }; next(); if (token.type === 'operator') { body.operator = token.value; } next(); if (token.type === 'identifier') { body.right = { type: 'identifier', identifierName: token.value }; } } return body; }; // 指针后移的函数 const next = () => { do { ++current; token = tokens[current] ? tokens[current] : { type: 'eof', value: '' }; } while (token.type === 'whitespace'); }; // 指针暂存的函数 const setTem = () => { tem.push(current); }; // 指针回退的函数 const backTem = () => { current = tem.pop(); token = tokens[current]; }; const ast = { type: 'Program', body: [] }; while (current < tokens.length) { const statement = parseDeclarations(); if (!statement) { break; } ast.body.push(statement); } return ast; };
至此咱们暴力 parser 了token 流,最终获得了简陋的抽象语法树:
{ "type": "Program", "body": [ { "type": "VariableDeclaration", "identifierName": "add", "init": { "type": "ArrowFunctionExpression", "params": [ { "type": "identifier", "identifierName": "a" }, { "type": "identifier", "identifierName": "b" } ], "body": { "type": "BinaryExpression", "left": { "type": "identifier", "identifierName": "a" }, "operator": "+", "right": { "type": "identifier", "identifierName": "b" } } } } ] }
在 Babel 中咱们使用者最常使用的地方就是代码转换,你们经常使用的 Babel 插件就是定义代码转换规则而生的,而代码解析和生成这一头一尾都主要是 Babel 负责。
好比咱们要用 babel 作一个React 转小程序的转换器,babel工做流程的粗略状况是这样的:
例如 Taro就是用 babel 完成的小程序语法转换.
到这里你们就明白了,咱们转换代码的关键就是根据当前的抽象语法树,以咱们定义的规则生成新的抽象语法树,转换的过程就是生成新抽象语法树的过程.
抽象语法树是一个树状数据结构,咱们要生成新语法树,那么必定须要访问 AST 上的节点,所以咱们须要一个工具来遍历抽象语法树的节点.
const traverser = (ast, visitor) => { // 若是节点是数组那么遍历数组 const traverseArray = (array, parent) => { array.forEach((child) => { traverseNode(child, parent); }); }; // 遍历 ast 节点 const traverseNode = (node, parent) => { const method = visitor[node.type]; if (method) { method(node, parent); } switch (node.type) { case 'Program': traverseArray(node.body, node); break; case 'VariableDeclaration': traverseArray(node.init.params, node.init); break; case 'identifier': break; default: throw new TypeError(node.type); } }; traverseNode(ast, null); };
咱们要转换的代码const add = (a, b) => a + b
实际上是个变量声明,按理来说咱们要转换为es5的代码也应该是个变量声明,好比这种:
var add = function(a, b) { return a + b }
固然也能够不按规则,直接生成一个函数声明,像这样:
function add(a, b) { return a + b }
此次咱们把代码转换为一个es5的函数声明
咱们以前的遍历器traverser
接收两个参数,一个是 ast 节点对象,一个是 visitor,visitor本质是挂载不一样方法的 JavaScript 对象,visitor 也叫作访问者,顾名思义它会访问 ast 上每一个节点,而后根据针对不一样节点用相应的方法作出不一样的转换.
const transformer = (ast) => { // 新 ast const newAst = { type: 'Program', body: [] }; // 在老 ast 上加一个指针指向新 ast ast._context = newAst.body; traverser(ast, { // 对于变量声明的处理方法 VariableDeclaration: (node, parent) => { let functionDeclaration = { params: [] }; if (node.init.type === 'ArrowFunctionExpression') { functionDeclaration.type = 'FunctionDeclaration'; functionDeclaration.identifierName = node.identifierName; } if (node.init.body.type === 'BinaryExpression') { functionDeclaration.body = { type: 'BlockStatement', body: [{ type: 'ReturnStatement', argument: node.init.body }], }; } parent._context.push(functionDeclaration); }, //对于字符的处理方法 identifier: (node, parent) => { if (parent.type === 'ArrowFunctionExpression') { // 忽略我这暴力的操做....领略大意便可.. ast._context[0].params.push({ type: 'identifier', identifierName: node.identifierName }); } } }); return newAst; };
咱们以前提到过,生成代码这一步其实是根据咱们转换后的抽象语法树来生成新的代码,咱们会实现一个函数, 他接受一个对象( ast),经过递归生成最终的代码
const generator = (node) => { switch (node.type) { // 若是是 `Program` 结点,那么咱们会遍历它的 `body` 属性中的每个结点,而且递归地 // 对这些结点再次调用 codeGenerator,再把结果打印进入新的一行中。 case 'Program': return node.body.map(generator) .join('\n'); // 若是是FunctionDeclaration咱们分别遍历调用其参数数组以及调用其 body 的属性 case 'FunctionDeclaration': return 'function' + ' ' + node.identifierName + '(' + node.params.map(generator) + ')' + ' ' + generator(node.body); // 对于 `Identifiers` 咱们只是返回 `node` 的 identifierName case 'identifier': return node.identifierName; // 若是是BlockStatement咱们遍历调用其body数组 case 'BlockStatement': return '{' + node.body.map(generator) + '}'; // 若是是ReturnStatement咱们调用其 argument 的属性 case 'ReturnStatement': return 'return' + ' ' + generator(node.argument); // 若是是ReturnStatement咱们调用其左右节点并拼接 case 'BinaryExpression': return generator(node.left) + ' ' + node.operator + ' ' + generator(node.right); // 没有符合的则报错 default: throw new TypeError(node.type); } };
至此咱们完成了一个简陋的微型 babel,咱们开始试验:
const compiler = (input) => { const tokens = tokenizer(input); const ast = parser(tokens); const newAst = transformer(ast); const output = generator(newAst); return output; }; const str = 'const add = (a, b) => a + b'; const result = compiler(str); console.log(result); // function add(a,b) {return a + b}
咱们成功地将一个es6的箭头函数转换为es5的function函数.
咱们能够经过这个微型 babel 了解 babel 的工做原理,若是让你对编译原理产生兴趣并去深刻那是更好的, babel集合包 是有数十万行代码的巨大工程,咱们用区区几百行代码只能展现其最基本的原理,代码有不少不合理之处,若是想真正的了解 babel 欢迎阅读器源码.
前端能够利用编译原理相关的东西还有不少,除了咱们常见的es6转换工具 babel,代码检测的 eslint等等,咱们还能够:
这篇文章受the-super-tiny-compiler启发而来.
想要实时关注笔者最新的文章和最新的文档更新请关注公众号程序员面试官,后续的文章会优先在公众号更新.
简历模板: 关注公众号回复「模板」获取
《前端面试手册》: 配套于本指南的突击手册,关注公众号回复「fed」获取