let esprima = require("esprima");
let estraverse = require("estraverse");
let code = "function add(a,b){return a + b}";
let tree = esprima.parseScript(code);
// 遍历语法树 tree
estraverse.traverse(tree, {
// 监听函数 进入阶段
enter(node) {
console.log("enter", node.type);
},
// 监听函数 离开阶段
leave(node) {
console.log("leave", node.type);
}
});
控制台打印:
enter Program
enter FunctionDeclaration
enter Identifier
leave Identifier
enter Identifier
leave Identifier
enter Identifier
leave Identifier
enter BlockStatement
enter ReturnStatement
enter BinaryExpression
enter Identifier
leave Identifier
enter Identifier
leave Identifier
leave BinaryExpression
leave ReturnStatement
leave BlockStatement
leave FunctionDeclaration
leave Program
estraverse 模块的 traverse 方法能遍历 AST ,它有两个参数,第一个就是 AST,第二个参数是遍历的操作函数,遍历有两个阶段,一个是 enter 进入阶段,另一个是 leave 离开阶段,这两个监听函数都有一个参数,就是遍历的每个节点,上述代码中打印了每个节点的 type 属性,在真实操作中,会在这个 enter 阶段根据实际需要修改相应节点的值。
let esprima = require("esprima");
let estraverse = require("estraverse");
let escodegen = require("escodegen");
let code = "function add(a,b){return a + b}";
let tree = esprima.parseScript(code);
// 遍历语法树 tree
estraverse.traverse(tree, {
// 将加改成减
enter(node) {
if (node.type == "FunctionDeclaration") {
node.id.name = "sub";
}
if (node.type == "BinaryExpression") {
node.operator = "-";
}
}
});
let result = escodegen.generate(tree);
抽象语法树 (AST)
AST 在日常的开发中很难用到,是因为有人已经帮我们做了,例如 webpack 的 loader,babel 等等,都有现成的库可以用,所以,我们基本上都没有用到,但是,要想提高,就必须懂得 AST。
AST 的功能非常强大,可以说现代前端 javascript 的精髓就是 AST。
从一个简单的函数说起
使用在线工具 astexplorer
左边是源函数,右边展示的是转换后的该函数的抽象树形结构,下面逐一分析字段:
抽象语法树是一个对象,该对象会有一个顶级的
type
属性Program
,第二个属性是body
,它是一个数组,该数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息。转换这个函数,重点看下抽象语法树结构:
分析这个树形结构,主要分析最顶层的
body
字段下的数据,首先这个函数是一个FunctionDeclaration
对象,它有三个重要字段:type
为Identifier
字段是最基础的标志符,用来定义对象,name
为add
,就是这个函数的名字type
为BlockStatement
,表示块状域,是一个函数体{return a + b}
这个函数体树形结构中有个
body
字段,type
为ReturnStatement
,表示return
域,表示return a + b
type
属性BinaryExpression
表示二元运算表达式节点,left
和right
表示运算符左右的两个表达式,operator
表示一个二元运算符以上是一个最基础的抽象语法树,下面将利用对应的库来对它进行遍历,修改,并重新编译等操作。
esprima、estraverse 和 escodegen 三个核心库
这三个库就是为了操作 ast 的,也是实现
babel
的核心库。控制台打印:
esprima
还有两个方法parseModule
和tokenize
,parseModule
将 js 代码转换成一个模块,tokenize
就是按照一定的规则,例如 token 令牌(通常代表关键字,变量名,语法符号等),将代码分割为一个个的“串”,也就是语法单元)。涉及到词法解析的时候,常会用到tokenize
。控制台打印:
estraverse
模块的traverse
方法能遍历 AST ,它有两个参数,第一个就是 AST,第二个参数是遍历的操作函数,遍历有两个阶段,一个是enter
进入阶段,另一个是leave
离开阶段,这两个监听函数都有一个参数,就是遍历的每个节点,上述代码中打印了每个节点的type
属性,在真实操作中,会在这个enter
阶段根据实际需要修改相应节点的值。如,将加改成减,就会在
enter
阶段,根据node.type
字段,逐一判断,然后修改:控制台打印:
escodegen.generate
这个方法就是将修改后的 AST 重新编译成了 js 代码。babel 的应用
学会了上面的三个核心库,接下来进入到了
babel
实战了,俗话说,没有一个 js 的问题是一个babel
插件解决不了的,如果是就两个,写babel
插件已然成为了一个高阶前端必备的技能,下面就来写几个案例,熟悉babel
插件的基本操作。参考 babel 插件手册
babel-core 和 babel-types 的应用
这两个库的就是上面三个核心库的一个应用,它里面做了很多操作封装,尤其是
babel-types
,简直就是一个babel
的lodash
,判断工具和替换工具,生成工具都能从里面找到。先看一个箭头函数的例子:
控制台打印:
看到这个结果,我们来对
babel-core
和babel-types
做一个简单的总结。babel-core
主要用来做转换操作,babel.transform
首先将原 js 转换成 ast,然后进行遍历,在通过visitor
这个访问者,对特定的节点进行修改替换。babel-types
这个库上个代码中用到了几个工具方法:types.isBlockStatement
方法判断是否是块状域types.returnStatement
设置返回域types.blockStatement
设置块状 {}types.functionExpression
生成 FunctionExpression ast