共计 2633 个字符,预计需要花费 7 分钟才能阅读完成。
直奔主题
对于 js,AST 能干什么?
- babel 将 es6 转 es5
- mpvue、taro 等将 js 转为小程序
- 定制插件删除注释、console 等
ps: 本文只探讨 AST 的概念以及使用,编译原理的其他知识不做太多描述
工具库
@babel/core
- 用来解析 AST 以及将 AST 生成代码
@babel/types
- 构建新的 AST 节点
前置知识 – 编译原理概述
毫无疑问 js 是一个解释型语言,有疑问可以参考这篇文章
所以这里只简单描述一下 js 的编译过程(大雾),有兴趣了解编译型语言详细编译过程的可以看这本《编译原理》
- 词法分析:
实际就是从左到右一个字一个字地扫描分解,识别出单词、符号等。例如:var num = 1
会被分解成var
,num
,=
,1
- 语法分析
将词法分析的结果,根据语法规则转化为一个嵌套的对象,也就是AST
(Abstract Syntax Tree 即抽象语法树
) - 目标代码产生
将 AST 转换为可执行代码
生成 AST
demo.js
是我随便 copy 来的一段代码
isLeapYear()
function isLeapYear(year) {
const cond1 = year % 4 == 0; // 条件 1:年份必须要能被 4 整除
const cond2 = year % 100 != 0; // 条件 2:年份不能是整百数
const cond3 = year % 400 ==0; // 条件 3:年份是 400 的倍数
const cond = cond1 && cond2 || cond3;
console.log(cond)
if(cond) {alert(year + "是闰年");
return true;
} else {alert(year + "不是闰年");
return false;
}
}
现在我要把它转成 AST,这里使用 @babel/core
来解析,它提供了一个 parse
方法来将代码转化为 AST。
parse.ts
就是我的解析工具
import * as fs from 'fs'
import * as path from 'path'
import {parse} from '@babel/core'
const js_path = path.resolve(__dirname, '../demo.js')
let code = fs.readFileSync(js_path, {encoding: 'utf-8'})
const js_ast = parse(code)
console.log(js_ast)
可以看到 AST 结果如下:
结果太长就不一一解析了,只说 type
属性,就表示了这一行代码做了什么,VariableDeclaration
就表示这是一句声明语句, CallExpression
则代表这是一个调用函数的语句
将 AST 转回代码
@babel/core
提供了一个 transform
方法,输入代码和修改代码的规则,输出修改过的 AST,它看起来是这样的:
const ArrowPlugins = {
visitor: {VariableDeclaration(path: NodePath) {// ...},
CallExpression(path: NodePath) {// ...}
}
}
const d = transform(code, {
plugins: [ArrowPlugins]
})
当命中对应的 type 时就会走进相应的回调函数,接下来写个小????,将 alert
,console.log
以及 全部注释
都删除,然后将 ==
和 !=
改成 ===
和!==
完整代码
import * as fs from 'fs'
import * as path from 'path'
import {transform, parse, NodePath} from '@babel/core'
import {VariableDeclaration, CallExpression, MemberExpression, Identifier, BinaryExpression} from '@babel/types'
const js_path = path.resolve(__dirname, '../demo.js')
let code = fs.readFileSync(js_path, {encoding: 'utf-8'})
// const js_ast = parse(code)
// debugger
const ArrowPlugins = {
visitor: {VariableDeclaration(path: NodePath) { // 修改 == -> ===
const node = path.node as VariableDeclaration
node.declarations.map((item) => {
const init = item.init as BinaryExpression
const equalMap = {
'==': '===',
'!=': '!=='
}
init.operator = equalMap[init.operator] || init.operator
})
// 删除注释
delete node.leadingComments
delete node.trailingComments
},
CallExpression(path: NodePath) { // 调用函数
const node = path.node as CallExpression
// 删除 console.xxx 和 alert
const memberExpressionCallee = node.callee as MemberExpression
const identifierCallee = node.callee as Identifier
const object = memberExpressionCallee.object as Identifier
if (object && object.name === 'console' || identifierCallee.name === 'alert') {path.remove()
}
// 删除注释
delete node.leadingComments
delete node.trailingComments
}
}
}
const d = transform(code, {
plugins: [ArrowPlugins]
})
console.log(d.code)
总结
只是简单地使用了一下 @babel
提供的方法将代码转成 AST,并在树枝上做一些简单的修修改改,最后转成目标代码,如果只是日常使用或者用来自己写 babel 插件一般是足够了,想要了解更多的编译原理知识需要更系统的学习。
等我看完《编译原理》(大雾)
,再继续更新系列文章
正文完
发表至: javascript
2019-07-09