AST抽象语法树 Javascript版
在javascript世界中,你可以认为抽象语法树(AST)是最底层。 再往下,就是关于转换和编译的“黑魔法”领域了。
现在,我们拆解一个简单的add函数
function add(a, b) {
return a + b
}
首先,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象。
用力拆开,它成了三块:
- 一个id,就是它的名字,即add
- 两个params,就是它的参数,即[a, b]
- 一块body,也就是大括号内的一堆东西
add没办法继续拆下去了,它是一个最基础Identifier(标志)对象,用来作为函数的唯一标志。
{
name: 'add'
type: 'identifier'
...
}
params继续拆下去,其实是两个Identifier组成的数组。之后也没办法拆下去了。
[
{
name: 'a'
type: 'identifier'
...
},
{
name: 'b'
type: 'identifier'
...
}
]
接下来,我们继续拆开body
我们发现,body其实是一个BlockStatement(块状域)对象,用来表示是{return a + b}
打开Blockstatement,里面藏着一个ReturnStatement(Return域)对象,用来表示return a + b
继续打开ReturnStatement,里面是一个BinaryExpression(二项式)对象,用来表示a + b
继续打开BinaryExpression,它成了三部分,left
,operator
,right
operator
即+
left
里面装的,是Identifier对象a
right
里面装的,是Identifer对象b
就这样,我们把一个简单的add函数拆解完毕。
抽象语法树(Abstract Syntax Tree),的确是一种标准的树结构。
那么,上面我们提到的Identifier、Blockstatement、ReturnStatement、BinaryExpression, 这一个个小部件的说明书去哪查?
请查看 AST对象文档
recast
输入命令:npm i recast -S
你即可获得一把操纵语法树的螺丝刀
接下来,你可以在任意js文件下操纵这把螺丝刀,我们新建一个parse.js示意:
创建parse.js文件
// 给你一把"螺丝刀"——recast
const recast = require("recast");
// 你的"机器"——一段代码
// 我们使用了很奇怪格式的代码,想测试是否能维持代码结构
const code =
`
function add(a, b) {
return a +
// 有什么奇怪的东西混进来了
b
}
`
// 用螺丝刀解析机器
const ast = recast.parse(code);
// ast可以处理很巨大的代码文件
// 但我们现在只需要代码块的第一个body,即add函数
const add = ast.program.body[0]
console.log(add)
输入node parse.js
你可以查看到add函数的结构,与之前所述一致,通过AST对象文档可查到它的具体属性:
FunctionDeclaration{
type: 'FunctionDeclaration',
id: ...
params: ...
body: ...
}
recast.types.builders 制作模具
recast.types.builders里面提供了不少“模具”,让你可以轻松地拼接成新的机器。
最简单的例子,我们想把之前的function add(a, b){...}
声明,改成匿名函数式声明const add = function(a ,b){...}
如何改装?
第一步,我们创建一个VariableDeclaration变量声明对象,声明头为const, 内容为一个即将创建的VariableDeclarator对象。
第二步,创建一个VariableDeclarator,放置add.id在左边, 右边是将创建的FunctionDeclaration对象
第三步,我们创建一个FunctionDeclaration,如前所述的三个组件,id params body中,因为是匿名函数id设为空,params使用add.params,body使用add.body。
这样,就创建好了const add = function(){}
的AST对象。
在之前的parse.js代码之后,加入以下代码
// 引入变量声明,变量符号,函数声明三种“模具”
const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders
// 将准备好的组件置入模具,并组装回原来的ast对象。
ast.program.body[0] = variableDeclaration("const", [
variableDeclarator(add.id, functionExpression(
null, // 匿名化函数表达式.
add.params,
add.body
))
]);
//将AST对象重新转回可以阅读的代码
//这一行其实是recast.parse的逆向过程,具体公式为
//recast.print(recast.parse(source)).code === source
const output = recast.print(ast).code;
console.log(output)
打印出来还保留着“原装”的函数内容,连注释都没有变。
我们其实也可以打印出美化格式的代码段:
const output = recast.prettyPrint(ast, { tabWidth: 2 }).code
//输出为
const add = function(a, b) {
return a + b;
};
实战进阶:命令行修改js文件
除了parse/print/builder以外,Recast的三项主要功能:
- run: 通过命令行读取js文件,并转化成ast以供处理。
- tnt: 通过assert()和check(),可以验证ast对象的类型。
- visit: 遍历ast树,获取有效的AST对象并进行更改。
通过一个系列小务来学习全部的recast工具库:
demo.js
function add(a, b) {
return a + b
}
function sub(a, b) {
return a - b
}
function commonDivision(a, b) {
while (b !== 0) {
if (a > b) {
a = sub(a, b)
} else {
b = sub(b, a)
}
}
return a
}
recast.run 命令行文件读取
新建一个名为read.js
的文件,写入
read.js
recast.run( function(ast, printSource){
printSource(ast)
})
命令行输入
node read demo.js
我们查以看到js文件内容打印在了控制台上。
我们可以知道,node read
可以读取demo.js
文件,并将demo.js内容转化为ast对象。
同时它还提供了一个printSource
函数,随时可以将ast的内容转换回源码,以方便调试。
recast.visit AST节点遍历
read.js
#!/usr/bin/env node
const recast = require('recast')
recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function({node}) {
console.log(node)
return false
}
});
});
recast.visit将AST对象内的节点进行逐个遍历。
注意
- 你想操作函数声明,就使用visitFunctionDelaration遍历,想操作赋值表达式,就使用visitExpressionStatement。 只要在 AST对象文档中定义的对象,在前面加visit,即可遍历。
- 通过node可以取到AST对象
- 每个遍历函数后必须加上return false,或者选择以下写法,否则报错:
#!/usr/bin/env node
const recast = require('recast')
recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function(path) {
const node = path.node
printSource(node)
this.traverse(path)
}
})
});
调试时,如果你想输出AST对象,可以console.log(node)
如果你想输出AST对象对应的源码,可以printSource(node)
命令行输入 node read demo.js
进行测试。
#!/usr/bin/env node
在所有使用recast.run()
的文件顶部都需要加入这一行,它的意义我们最后再讨论。
TNT 判断AST对象类型
TNT,即recast.types.namedTypes,它用来判断AST对象是否为指定的类型。
TNT.Node.assert(),就像在机器里埋好的摧毁器,当机器不能完好运转时(类型不匹配),就摧毁机器(报错退出)
TNT.Node.check(),则可以判断类型是否一致,并输出False和True
上述Node可以替换成任意AST对象
例如:TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert()
read.js
#!/usr/bin/env node
const recast = require("recast");
const TNT = recast.types.namedTypes
recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function(path) {
const node = path.value
// 判断是否为ExpressionStatement,正确则输出一行字。
if(TNT.ExpressionStatement.check(node)){
console.log('这是一个ExpressionStatement')
}
this.traverse(path);
}
});
});
read.js
#!/usr/bin/env node
const recast = require("recast");
const TNT = recast.types.namedTypes
recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function(path) {
const node = path.node
// 判断是否为ExpressionStatement,正确不输出,错误则全局报错
TNT.ExpressionStatement.assert(node)
this.traverse(path);
}
});
});
实战:用AST修改源码,导出全部方法
exportific.js
现在,我们想让这个文件中的函数改写成能够全部导出的形式,例如
function add (a, b) {
return a + b
}
想改变为
exports.add = (a, b) => {
return a + b
}
首先,我们先用builders凭空实现一个键头函数
exportific.js
#!/usr/bin/env node
const recast = require("recast");
const {
identifier:id,
expressionStatement,
memberExpression,
assignmentExpression,
arrowFunctionExpression,
blockStatement
} = recast.types.builders
recast.run(function(ast, printSource) {
// 一个块级域 {}
console.log('\n\nstep1:')
printSource(blockStatement([]))
// 一个键头函数 ()=>{}
console.log('\n\nstep2:')
printSource(arrowFunctionExpression([],blockStatement([])))
// add赋值为键头函数 add = ()=>{}
console.log('\n\nstep3:')
printSource(assignmentExpression('=',id('add'),arrowFunctionExpression([],blockStatement([]))))
// exports.add赋值为键头函数 exports.add = ()=>{}
console.log('\n\nstep4:')
printSource(expressionStatement(assignmentExpression('=',memberExpression(id('exports'),id('add')),
arrowFunctionExpression([],blockStatement([])))))
});
上面写了我们一步一步推断出exports.add = ()=>{}
的过程,从而得到具体的AST结构体。
使用node exportific demo.js
运行可查看结果。
接下来,只需要在获得的最终的表达式中,把id('add')替换成遍历得到的函数名,把参数替换成遍历得到的函数参数,把blockStatement([])替换为遍历得到的函数块级作用域,就成功地改写了所有函数!
另外,我们需要注意,在commonDivision函数内,引用了sub函数,应改写成exports.sub
exportific.js
#!/usr/bin/env node
const recast = require("recast");
const {
identifier: id,
expressionStatement,
memberExpression,
assignmentExpression,
arrowFunctionExpression
} = recast.types.builders
recast.run(function (ast, printSource) {
// 用来保存遍历到的全部函数名
let funcIds = []
recast.types.visit(ast, {
// 遍历所有的函数定义
visitFunctionDeclaration(path) {
//获取遍历到的函数名、参数、块级域
const node = path.node
const funcName = node.id
const params = node.params
const body = node.body
// 保存函数名
funcIds.push(funcName.name)
// 这是上一步推导出来的ast结构体
const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
arrowFunctionExpression(params, body)))
// 将原来函数的ast结构体,替换成推导ast结构体
path.replace(rep)
// 停止遍历
return false
}
})
recast.types.visit(ast, {
// 遍历所有的函数调用
visitCallExpression(path){
const node = path.node;
// 如果函数调用出现在函数定义中,则修改ast结构
if (funcIds.includes(node.callee.name)) {
node.callee = memberExpression(id('exports'), node.callee)
}
// 停止遍历
return false
}
})
// 打印修改后的ast源码
printSource(ast)
})
一步到位,发一个最简单的exportific前端工具
以下代码添加作了两个小改动
- 添加说明书--help,以及添加了--rewrite模式,可以直接覆盖文件或默认为导出*.export.js文件。
- 将之前代码最后的 printSource(ast)替换成 writeASTFile(ast,filename,rewriteMode)
exportific.js
#!/usr/bin/env node
const recast = require("recast");
const {
identifier: id,
expressionStatement,
memberExpression,
assignmentExpression,
arrowFunctionExpression
} = recast.types.builders
const fs = require('fs')
const path = require('path')
// 截取参数
const options = process.argv.slice(2)
//如果没有参数,或提供了-h 或--help选项,则打印帮助
if(options.length===0 || options.includes('-h') || options.includes('--help')){
console.log(`
采用commonjs规则,将.js文件内所有函数修改为导出形式。
选项: -r 或 --rewrite 可直接覆盖原有文件
`)
process.exit(0)
}
// 只要有-r 或--rewrite参数,则rewriteMode为true
let rewriteMode = options.includes('-r') || options.includes('--rewrite')
// 获取文件名
const clearFileArg = options.filter((item)=>{
return !['-r','--rewrite','-h','--help'].includes(item)
})
// 只处理一个文件
let filename = clearFileArg[0]
const writeASTFile = function(ast, filename, rewriteMode){
const newCode = recast.print(ast).code
if(!rewriteMode){
// 非覆盖模式下,将新文件写入*.export.js下
filename = filename.split('.').slice(0,-1).concat(['export','js']).join('.')
}
// 将新代码写入文件
fs.writeFileSync(path.join(process.cwd(),filename),newCode)
}
recast.run(function (ast, printSource) {
let funcIds = []
recast.types.visit(ast, {
visitFunctionDeclaration(path) {
//获取遍历到的函数名、参数、块级域
const node = path.node
const funcName = node.id
const params = node.params
const body = node.body
funcIds.push(funcName.name)
const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
arrowFunctionExpression(params, body)))
path.replace(rep)
return false
}
})
recast.types.visit(ast, {
visitCallExpression(path){
const node = path.node;
if (funcIds.includes(node.callee.name)) {
node.callee = memberExpression(id('exports'), node.callee)
}
return false
}
})
writeASTFile(ast,filename,rewriteMode)
})
现在尝试一下
node exportific demo.js
已经可以在当前目录下找到源码变更后的demo.export.js
文件了。
npm发包
编辑一下package.json文件
{
"name": "exportific",
"version": "0.0.1",
"description": "改写源码中的函数为可exports.XXX形式",
"main": "exportific.js",
"bin": {
"exportific": "./exportific.js"
},
"keywords": [],
"author": "wanthering",
"license": "ISC",
"dependencies": {
"recast": "^0.15.3"
}
}
注意bin选项,它的意思是将全局命令exportific
指向当前目录下的exportific.js
这时,输入npm link
就在本地生成了一个exportific
命令。
之后,只要哪个js文件想导出来使用,就exportific XXX.js
一下。
一定要注意exportific.js文件头有
#!/usr/bin/env node
接下来,正式发布npm包!
如果你已经有了npm 帐号,请使用npm login
登录
如果你还没有npm帐号 https://www.npmjs.com/signup 非常简单就可以注册npm
然后,输入
npm publish
没有任何繁琐步骤,丝毫审核都没有,你就发布了一个实用的前端小工具exportific 。任何人都可以通过
npm i exportific -g
全局安装这一个插件。
提示:在试验教程时,请不要和我的包重名,修改一下发包名称。
#!/usr/bin/env node
不同用户或者不同的脚本解释器有可能安装在不同的目录下,系统如何知道要去哪里找你的解释程序呢? /usr/bin/env
就是告诉系统可以在PATH目录中查找。 所以配置#!/usr/bin/env node
, 就是解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。
如果出现No such file or directory
的错误?因为你的node安装路径没有添加到系统的PATH中。所以去进行node环境变量配置就可以了。
要是你只是想简单的测试一下,那么你可以通过which node
命令来找到你本地的node安装路径,将/usr/bin/env
改为你查找到的node路径即可。
参考文章:https://segmentfault.com/a/1190000016231512?utm_source=tag-newest#comment-area
AST抽象语法树 Javascript版的更多相关文章
- AST抽象语法树
抽象语法树简介 (一)简介 抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并 ...
- 用python演示一个简单的AST(抽象语法树)
如果对'a + 3 * b'进行解释,当中a=2,b=5 代码非常easy,就不再进行具体的解释了. Num = lambda env, n: n Var = lambda env, x: env[x ...
- 【深入】 - AST抽象语法树
参考: https://segmentfault.com/a/1190000016231512
- 从零写一个编译器(九):语义分析之构造抽象语法树(AST)
项目的完整代码在 C2j-Compiler 前言 在上一篇完成了符号表的构建,下一步就是输出抽象语法树(Abstract Syntax Tree,AST) 抽象语法树(abstract syntax ...
- 如何查看SparkSQL 生成的抽象语法树?
前言 在<Spark SQL内核剖析>书中4.3章节,谈到Catalyst体系中生成的抽象语法树的节点都是以Context来结尾,在ANLTR4以及生成的SqlBaseParser解析SQ ...
- javascript编写一个简单的编译器(理解抽象语法树AST)
javascript编写一个简单的编译器(理解抽象语法树AST) 编译器 是一种接收一段代码,然后把它转成一些其他一种机制.我们现在来做一个在一张纸上画出一条线,那么我们画出一条线需要定义的条件如下: ...
- JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧
这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...
- Babel(抽象语法树,又称AST)
文章:https://juejin.im/post/5a9315e46fb9a0633a711f25 https://github.com/jamiebuilds/babel-handbook/blo ...
- 理解Babel是如何编译JS代码的及理解抽象语法树(AST)
Babel是如何编译JS代码的及理解抽象语法树(AST) 1. Babel的作用是? 很多浏览器目前还不支持ES6的代码,但是我们可以通过Babel将ES6的代码转译成ES5代码,让所有的浏览器都 ...
随机推荐
- [学习笔记] [数据分析] 01.Python入门
1.安装Python与环境配置 ① ② 安装pip以及利用pip安装Python库 2.Anaconda安装 conda list 要在root环境下 3.常用数据分析库 ① Numpy 安装:con ...
- 基于springboot的web项目最佳实践
springboot 可以说是现在做javaweb开发最火的技术,我在基于springboot搭建项目的过程中,踩过不少坑,发现整合框架时并非仅仅引入starter 那么简单. 要做到简单,易用,扩展 ...
- Kubernetes增强型调度器Volcano算法分析
[摘要] Volcano 是基于 Kubernetes 的批处理系统,源自于华为云开源出来的.Volcano 方便 AI.大数据.基因.渲染等诸多行业通用计算框架接入,提供高性能任务调度引擎,高性能异 ...
- C#中的Stopwatch类简单使用
Stopwatch实例可以度量一个间隔的运行时间, 或度量多个间隔内所用时间的总和. 命名空间System.Diagnostics. 简单使用 using System; using System.D ...
- 手撕 JVM 垃圾收集日志
下图是本篇的写作大纲,将从以下四个方面介绍怎么样处理 JVM 日志. 有准备才能不慌 想要分析日志,首先你得有日志呀,对不对.凡是未雨绸蒙总是没错的.所谓有日志的意思,你要把 JVM 参数配置好,日志 ...
- 创建自己的github仓库
作者: wangzz 原文地址:http://blog.csdn.net/wzzvictory/article/details/20067595 一.创建自己的github仓库 CocoaPods都托 ...
- 针对tomcat中startup启动服务器闪退的情况
1.要保证你配置jdk环境变量无误:java环境变量配置详解. 2. 3.在环境变量中设置CATALINA_HOME:
- 2019 ICPC上海网络赛 A 题 Lightning Routing I (动态维护树的直径)
题目: 给定一棵树, 带边权. 现在有2种操作: 1.修改第i条边的权值. 2.询问u到其他一个任意点的最大距离是多少. 题解: 树的直径可以通过两次 dfs() 的方法求得.换句话说,到任意点最远的 ...
- SPOJ Free TourII(点分治+启发式合并)
After the success of 2nd anniversary (take a look at problem FTOUR for more details), this 3rd year, ...
- Balls in the Boxes
Description Mr. Mindless has many balls and many boxes,he wants to put all the balls into some of th ...