BABLE 基础
前言
Babel 是现代 JavaScript 语法转换器,几乎在任何现代前端项目中都能看到他的身影,其背后的原理对于大部分开发者还属于黑盒,不过 Babel 作为一个工具真的有了解背后原理的必要吗?
如果只是 Babel 可能真没有必要,问题是其背后的原理在我们开发中应用过于广泛了,包括不限于: eslint jshint stylelint css-in-js prettier jsx vue-template uglify-js postcss less 等等等等,从模板到代码检测,从混淆压缩到代码转换,甚至编辑器的代码高亮都与之息息相关.
如果有兴趣就可以搞一些黑魔法: 前端工程师可以用编译原理做什么?
前置
Babel 大概分为三大部分:
解析: 将代码(其实就是字符串)转换成 AST( 抽象语法树)
转换: 访问 AST 的节点进行变换操作生成新的 AST
生成: 以新的 AST 为基础生成代码
我们主要通过打造一个微型 babel 来了解 babel 的基本原理,这个微型 babel 的功能很单一也很鸡肋,但是依然有400行代码,其实现细节与 babel 并不相同,因为我们省去了很多额外的验证和信息解析,因为单单一个兼容现代 JavaScript 语法的 parser 就需要5000行代码,并不利于我们快速了解 babel 的基本实现,所以这个微型 babel可以说比较鸡肋(因为除了展示之外没啥用处),但是比较完整展示了 babel 的基本原理,你可以以此作为入门,在入门之后如果仍有兴趣,可以阅读:
acorn: 轻量级现代 JavaScript 解析器, babel 最初就是基于此项目
1. 代码解析
1.1 parser 概念
代码解析,也就是我们常说的 Parser, 用于将一段代码(文本)解析成一个数据结构.
例如这段 es6的代码
我们用 babel 解析后便是这种形式:
我们以解析上面的 es6箭头函数为目标,来写一个简单的 parser.
文本 ---> AST 的过程中有两个关键步骤:
词法分析: 将代码(字符串)分割为token流,即语法单元成的数组
语法分析: 分析token流(上面生成的数组)并生成 AST
1.2 词法分析(Tokenizer -- 词法分析器)
要做词法分析,首先我们需要明白在 JavaScript 中哪些属于语法单元
数字:JavaScript 中的科学记数法以及普通数组都属于语法单元.
括号:『(』『)』只要出现,不管任何意义都算是语法单元
标识符:连续字符,常见的有变量,常量(例如: null true),关键字(if break)等等
运算符:+、-、*、/等等
当然还有注释,中括号等
在我们 parser 的过程中,应该换一个角度看待代码,我们平时工作用的代码.本质是就是字符串或者一段文本,它没有任何意义,是 JavaScript 引擎赋予了它意义,所以我们在解析过程中代码只是一段字符串.
仍然以下面代码为例
我们期望的结果是类似这样的
那么我们现在开始打造一个Tokenizer(词法分析器)
那么我们基本的词法分析器就打造完成,因为只针对这一个es6函数,所以没有做额外的工作(额外的工作量会非常庞大).
1.3 语法分析
语法分析要比词法分析复杂得多,因为我们接下来的是示意代码,所以做了很多“武断”的判断来省略代码,即使这样也是整个微型 babel 中代码量最多的.
语法分析之所以复杂,是因为要分析各种语法的可能性,需要开发者根据token流(上一节我们生成的 token 数组)提供的信息来分析出代码之间的逻辑关系,只有经过词法分析 token 流才能成为有结构的抽象语法树.
做语法分析最好依照标准,大多数 JavaScript Parser 都遵循estree规范
由于标准内容很多,感兴趣的可以去阅读,我们目前只介绍几个比较重要的标准:
语句(Statements): 语句是 JavaScript 中非常常见的语法,我们常见的循环、if 判断、异常处理语句、with 语句等等都属于语句
表达式(Expressions): 表达式是一组代码的集合,它返回一个值,表达式是另一个十分常见的语法,函数表达式就是一种典型的表达式,如果你不理解什么是表达式, MDN上有很详细的解释.
声明(Declarations): 声明分为变量声明和函数声明,表达式(Expressions)中的函数表达式的例子用声明的写法就是下面这样.
你可能有点糊涂,为了理清其中的关系,我们就下面的代码为例来解读
首先这段代码的整体本质是是一个变量声明(VariableDeclarator):
而变量被声明为一个函数表达式(FunctionExpression):
函数表达式中的大括号在内的为块状语句(BlockStatement):
块状语句内 return 的部分是返回语句(ReturnStatement):
而 return 的其实是一个二元运算符或者叫二元表达式(BinaryExpression):
上面提到的这些有些属于表达式,有些属于声明也有些属于语句,当然还有更多我们没提到的,它们被语法分析之后被叫做AST(抽象语法树).
我们做语法分析的时候思路也是类似的,要分析哪一层的 token 到底属于表达式或者说语句,如果是语句那么是块状语句(BlockStatement)还是Loops,如果是 Loops 那么属于while 循环(WhileStatement)还是for 循环(ForStatement)等等,其中甚至难免要考虑作用域的问题,因此语法分析的复杂也体现在此.
至此我们暴力 parser 了token 流,最终得到了简陋的抽象语法树:
2. 代码转换
2.1 如何转换代码?
在 Babel 中我们使用者最常使用的地方就是代码转换,大家常用的 Babel 插件就是定义代码转换规则而生的,而代码解析和生成这一头一尾都主要是 Babel 负责。
比如我们要用 babel 做一个React 转小程序的转换器,babel工作流程的粗略情况是这样的:
babel 将 React 代码解析为抽象语法树
开发者利用 babel 插件定义转换规则,根据原本的抽象语法树生成一个符合小程序规则的新抽象语法树
babel 则根据新的抽象语法树生成代码,此时的代码就是符合小程序规则的新代码
例如 Taro就是用 babel 完成的小程序语法转换.
到这里大家就明白了,我们转换代码的关键就是根据当前的抽象语法树,以我们定义的规则生成新的抽象语法树,转换的过程就是生成新抽象语法树的过程.
2.2 遍历抽象语法树(实现遍历器traverser)
抽象语法树是一个树状数据结构,我们要生成新语法树,那么一定需要访问 AST 上的节点,因此我们需要一个工具来遍历抽象语法树的节点.
2.3 转换代码(实现转换器transformer)
我们要转换的代码const add = (a, b) => a + b其实是个变量声明,按理来讲我们要转换为es5的代码也应该是个变量声明,比如这种:
当然也可以不按规则,直接生成一个函数声明,像这样:
这次我们把代码转换为一个es5的函数声明

我们之前的遍历器traverser接收两个参数,一个是 ast 节点对象,一个是 visitor,visitor本质是挂载不同方法的 JavaScript 对象,visitor 也叫做访问者,顾名思义它会访问 ast 上每个节点,然后根据针对不同节点用相应的方法做出不同的转换.
3. 生成代码(实现生成器generator)
我们之前提到过,生成代码这一步实际上是根据我们转换后的抽象语法树来生成新的代码,我们会实现一个函数, 他接受一个对象( ast),通过递归生成最终的代码
至此我们完成了一个简陋的微型 babel,我们开始试验:
我们成功地将一个es6的箭头函数转换为es5的function函数.
最后
我们可以通过这个微型 babel 了解 babel 的工作原理,如果让你对编译原理产生兴趣并去深入那是更好的, babel集合包 是有数十万行代码的巨大工程,我们用区区几百行代码只能展示其最基本的原理,代码有很多不合理之处,如果想真正的了解 babel 欢迎阅读器源码.
前端可以利用编译原理相关的东西还有很多,除了我们常见的es6转换工具 babel,代码检测的 eslint等等,我们还可以:
小程序多端转义 Taro
小程序热更新js 解释器
babel与错误监控浏览器端 JavaScript 异常监控
模板引擎
css 预处理后处理等等
...
Last updated
Was this helpful?