在javascript世界中,你可以认为抽象语法树(AST)是最底层。 再往下,就是关于转换和编译的“黑魔法”领域了。

现在,我们拆解一个简单的add函数

  1. function add(a, b) {
  2. return a + b
  3. }

首先,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象。

用力拆开,它成了三块:

  • 一个id,就是它的名字,即add
  • 两个params,就是它的参数,即[a, b]
  • 一块body,也就是大括号内的一堆东西

add没办法继续拆下去了,它是一个最基础Identifier(标志)对象,用来作为函数的唯一标志。

  1. {
  2. name: 'add'
  3. type: 'identifier'
  4. ...
  5. }

params继续拆下去,其实是两个Identifier组成的数组。之后也没办法拆下去了。

  1. [
  2. {
  3. name: 'a'
  4. type: 'identifier'
  5. ...
  6. },
  7. {
  8. name: 'b'
  9. type: 'identifier'
  10. ...
  11. }
  12. ]

接下来,我们继续拆开body

我们发现,body其实是一个BlockStatement(块状域)对象,用来表示是{return a + b}

打开Blockstatement,里面藏着一个ReturnStatement(Return域)对象,用来表示return a + b

继续打开ReturnStatement,里面是一个BinaryExpression(二项式)对象,用来表示a + b

继续打开BinaryExpression,它成了三部分,leftoperatorright

  • 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文件

  1. // 给你一把"螺丝刀"——recast
  2. const recast = require("recast");
  3. // 你的"机器"——一段代码
  4. // 我们使用了很奇怪格式的代码,想测试是否能维持代码结构
  5. const code =
  6. `
  7. function add(a, b) {
  8. return a +
  9. // 有什么奇怪的东西混进来了
  10. b
  11. }
  12. `
  13. // 用螺丝刀解析机器
  14. const ast = recast.parse(code);
  15. // ast可以处理很巨大的代码文件
  16. // 但我们现在只需要代码块的第一个body,即add函数
  17. const add = ast.program.body[0]
  18. console.log(add)

输入node parse.js你可以查看到add函数的结构,与之前所述一致,通过AST对象文档可查到它的具体属性:

  1. FunctionDeclaration{
  2. type: 'FunctionDeclaration',
  3. id: ...
  4. params: ...
  5. body: ...
  6. }

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代码之后,加入以下代码

  1. // 引入变量声明,变量符号,函数声明三种“模具”
  2. const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders
  3. // 将准备好的组件置入模具,并组装回原来的ast对象。
  4. ast.program.body[0] = variableDeclaration("const", [
  5. variableDeclarator(add.id, functionExpression(
  6. null, // 匿名化函数表达式.
  7. add.params,
  8. add.body
  9. ))
  10. ]);
  11. //将AST对象重新转回可以阅读的代码
  12. //这一行其实是recast.parse的逆向过程,具体公式为
  13. //recast.print(recast.parse(source)).code === source
  14. const output = recast.print(ast).code;
  15. console.log(output)

打印出来还保留着“原装”的函数内容,连注释都没有变。

我们其实也可以打印出美化格式的代码段:

const output = recast.prettyPrint(ast, { tabWidth: 2 }).code

  1. //输出为
  2. const add = function(a, b) {
  3. return a + b;
  4. };

实战进阶:命令行修改js文件

除了parse/print/builder以外,Recast的三项主要功能:

  • run: 通过命令行读取js文件,并转化成ast以供处理。
  • tnt: 通过assert()和check(),可以验证ast对象的类型。
  • visit: 遍历ast树,获取有效的AST对象并进行更改。

通过一个系列小务来学习全部的recast工具库:

demo.js

  1. function add(a, b) {
  2. return a + b
  3. }
  4. function sub(a, b) {
  5. return a - b
  6. }
  7. function commonDivision(a, b) {
  8. while (b !== 0) {
  9. if (a > b) {
  10. a = sub(a, b)
  11. } else {
  12. b = sub(b, a)
  13. }
  14. }
  15. return a
  16. }

recast.run 命令行文件读取

新建一个名为read.js的文件,写入

read.js

  1. recast.run( function(ast, printSource){
  2. printSource(ast)
  3. })

命令行输入

node read demo.js

我们查以看到js文件内容打印在了控制台上。

我们可以知道,node read可以读取demo.js文件,并将demo.js内容转化为ast对象。

同时它还提供了一个printSource函数,随时可以将ast的内容转换回源码,以方便调试。

recast.visit AST节点遍历

read.js

  1. #!/usr/bin/env node
  2. const recast = require('recast')
  3. recast.run(function(ast, printSource) {
  4. recast.visit(ast, {
  5. visitExpressionStatement: function({node}) {
  6. console.log(node)
  7. return false
  8. }
  9. });
  10. });

recast.visit将AST对象内的节点进行逐个遍历。

注意

  • 你想操作函数声明,就使用visitFunctionDelaration遍历,想操作赋值表达式,就使用visitExpressionStatement。 只要在 AST对象文档中定义的对象,在前面加visit,即可遍历。
  • 通过node可以取到AST对象
  • 每个遍历函数后必须加上return false,或者选择以下写法,否则报错:
  1. #!/usr/bin/env node
  2. const recast = require('recast')
  3. recast.run(function(ast, printSource) {
  4. recast.visit(ast, {
  5. visitExpressionStatement: function(path) {
  6. const node = path.node
  7. printSource(node)
  8. this.traverse(path)
  9. }
  10. })
  11. });

调试时,如果你想输出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
  1. #!/usr/bin/env node
  2. const recast = require("recast");
  3. const TNT = recast.types.namedTypes
  4. recast.run(function(ast, printSource) {
  5. recast.visit(ast, {
  6. visitExpressionStatement: function(path) {
  7. const node = path.value
  8. // 判断是否为ExpressionStatement,正确则输出一行字。
  9. if(TNT.ExpressionStatement.check(node)){
  10. console.log('这是一个ExpressionStatement')
  11. }
  12. this.traverse(path);
  13. }
  14. });
  15. });

read.js

  1. #!/usr/bin/env node
  2. const recast = require("recast");
  3. const TNT = recast.types.namedTypes
  4. recast.run(function(ast, printSource) {
  5. recast.visit(ast, {
  6. visitExpressionStatement: function(path) {
  7. const node = path.node
  8. // 判断是否为ExpressionStatement,正确不输出,错误则全局报错
  9. TNT.ExpressionStatement.assert(node)
  10. this.traverse(path);
  11. }
  12. });
  13. });

实战:用AST修改源码,导出全部方法

exportific.js

现在,我们想让这个文件中的函数改写成能够全部导出的形式,例如

  1. function add (a, b) {
  2. return a + b
  3. }

想改变为

  1. exports.add = (a, b) => {
  2. return a + b
  3. }

首先,我们先用builders凭空实现一个键头函数

exportific.js
  1. #!/usr/bin/env node
  2. const recast = require("recast");
  3. const {
  4. identifier:id,
  5. expressionStatement,
  6. memberExpression,
  7. assignmentExpression,
  8. arrowFunctionExpression,
  9. blockStatement
  10. } = recast.types.builders
  11. recast.run(function(ast, printSource) {
  12. // 一个块级域 {}
  13. console.log('\n\nstep1:')
  14. printSource(blockStatement([]))
  15. // 一个键头函数 ()=>{}
  16. console.log('\n\nstep2:')
  17. printSource(arrowFunctionExpression([],blockStatement([])))
  18. // add赋值为键头函数 add = ()=>{}
  19. console.log('\n\nstep3:')
  20. printSource(assignmentExpression('=',id('add'),arrowFunctionExpression([],blockStatement([]))))
  21. // exports.add赋值为键头函数 exports.add = ()=>{}
  22. console.log('\n\nstep4:')
  23. printSource(expressionStatement(assignmentExpression('=',memberExpression(id('exports'),id('add')),
  24. arrowFunctionExpression([],blockStatement([])))))
  25. });

上面写了我们一步一步推断出exports.add = ()=>{}的过程,从而得到具体的AST结构体。

使用node exportific demo.js运行可查看结果。

接下来,只需要在获得的最终的表达式中,把id('add')替换成遍历得到的函数名,把参数替换成遍历得到的函数参数,把blockStatement([])替换为遍历得到的函数块级作用域,就成功地改写了所有函数!

另外,我们需要注意,在commonDivision函数内,引用了sub函数,应改写成exports.sub

exportific.js
  1. #!/usr/bin/env node
  2. const recast = require("recast");
  3. const {
  4. identifier: id,
  5. expressionStatement,
  6. memberExpression,
  7. assignmentExpression,
  8. arrowFunctionExpression
  9. } = recast.types.builders
  10. recast.run(function (ast, printSource) {
  11. // 用来保存遍历到的全部函数名
  12. let funcIds = []
  13. recast.types.visit(ast, {
  14. // 遍历所有的函数定义
  15. visitFunctionDeclaration(path) {
  16. //获取遍历到的函数名、参数、块级域
  17. const node = path.node
  18. const funcName = node.id
  19. const params = node.params
  20. const body = node.body
  21. // 保存函数名
  22. funcIds.push(funcName.name)
  23. // 这是上一步推导出来的ast结构体
  24. const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
  25. arrowFunctionExpression(params, body)))
  26. // 将原来函数的ast结构体,替换成推导ast结构体
  27. path.replace(rep)
  28. // 停止遍历
  29. return false
  30. }
  31. })
  32. recast.types.visit(ast, {
  33. // 遍历所有的函数调用
  34. visitCallExpression(path){
  35. const node = path.node;
  36. // 如果函数调用出现在函数定义中,则修改ast结构
  37. if (funcIds.includes(node.callee.name)) {
  38. node.callee = memberExpression(id('exports'), node.callee)
  39. }
  40. // 停止遍历
  41. return false
  42. }
  43. })
  44. // 打印修改后的ast源码
  45. printSource(ast)
  46. })

一步到位,发一个最简单的exportific前端工具

以下代码添加作了两个小改动

  1. 添加说明书--help,以及添加了--rewrite模式,可以直接覆盖文件或默认为导出*.export.js文件。
  2. 将之前代码最后的 printSource(ast)替换成 writeASTFile(ast,filename,rewriteMode)

exportific.js

  1. #!/usr/bin/env node
  2. const recast = require("recast");
  3. const {
  4. identifier: id,
  5. expressionStatement,
  6. memberExpression,
  7. assignmentExpression,
  8. arrowFunctionExpression
  9. } = recast.types.builders
  10. const fs = require('fs')
  11. const path = require('path')
  12. // 截取参数
  13. const options = process.argv.slice(2)
  14. //如果没有参数,或提供了-h 或--help选项,则打印帮助
  15. if(options.length===0 || options.includes('-h') || options.includes('--help')){
  16. console.log(`
  17. 采用commonjs规则,将.js文件内所有函数修改为导出形式。
  18. 选项: -r 或 --rewrite 可直接覆盖原有文件
  19. `)
  20. process.exit(0)
  21. }
  22. // 只要有-r 或--rewrite参数,则rewriteMode为true
  23. let rewriteMode = options.includes('-r') || options.includes('--rewrite')
  24. // 获取文件名
  25. const clearFileArg = options.filter((item)=>{
  26. return !['-r','--rewrite','-h','--help'].includes(item)
  27. })
  28. // 只处理一个文件
  29. let filename = clearFileArg[0]
  30. const writeASTFile = function(ast, filename, rewriteMode){
  31. const newCode = recast.print(ast).code
  32. if(!rewriteMode){
  33. // 非覆盖模式下,将新文件写入*.export.js下
  34. filename = filename.split('.').slice(0,-1).concat(['export','js']).join('.')
  35. }
  36. // 将新代码写入文件
  37. fs.writeFileSync(path.join(process.cwd(),filename),newCode)
  38. }
  39. recast.run(function (ast, printSource) {
  40. let funcIds = []
  41. recast.types.visit(ast, {
  42. visitFunctionDeclaration(path) {
  43. //获取遍历到的函数名、参数、块级域
  44. const node = path.node
  45. const funcName = node.id
  46. const params = node.params
  47. const body = node.body
  48. funcIds.push(funcName.name)
  49. const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
  50. arrowFunctionExpression(params, body)))
  51. path.replace(rep)
  52. return false
  53. }
  54. })
  55. recast.types.visit(ast, {
  56. visitCallExpression(path){
  57. const node = path.node;
  58. if (funcIds.includes(node.callee.name)) {
  59. node.callee = memberExpression(id('exports'), node.callee)
  60. }
  61. return false
  62. }
  63. })
  64. writeASTFile(ast,filename,rewriteMode)
  65. })

现在尝试一下

node exportific demo.js

已经可以在当前目录下找到源码变更后的demo.export.js文件了。

npm发包

编辑一下package.json文件

  1. {
  2. "name": "exportific",
  3. "version": "0.0.1",
  4. "description": "改写源码中的函数为可exports.XXX形式",
  5. "main": "exportific.js",
  6. "bin": {
  7. "exportific": "./exportific.js"
  8. },
  9. "keywords": [],
  10. "author": "wanthering",
  11. "license": "ISC",
  12. "dependencies": {
  13. "recast": "^0.15.3"
  14. }
  15. }

注意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 。任何人都可以通过

  1. 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版的更多相关文章

  1. AST抽象语法树

    抽象语法树简介 (一)简介 抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并 ...

  2. 用python演示一个简单的AST(抽象语法树)

    如果对'a + 3 * b'进行解释,当中a=2,b=5 代码非常easy,就不再进行具体的解释了. Num = lambda env, n: n Var = lambda env, x: env[x ...

  3. 【深入】 - AST抽象语法树

    参考: https://segmentfault.com/a/1190000016231512

  4. 从零写一个编译器(九):语义分析之构造抽象语法树(AST)

    项目的完整代码在 C2j-Compiler 前言 在上一篇完成了符号表的构建,下一步就是输出抽象语法树(Abstract Syntax Tree,AST) 抽象语法树(abstract syntax ...

  5. 如何查看SparkSQL 生成的抽象语法树?

    前言 在<Spark SQL内核剖析>书中4.3章节,谈到Catalyst体系中生成的抽象语法树的节点都是以Context来结尾,在ANLTR4以及生成的SqlBaseParser解析SQ ...

  6. javascript编写一个简单的编译器(理解抽象语法树AST)

    javascript编写一个简单的编译器(理解抽象语法树AST) 编译器 是一种接收一段代码,然后把它转成一些其他一种机制.我们现在来做一个在一张纸上画出一条线,那么我们画出一条线需要定义的条件如下: ...

  7. JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...

  8. Babel(抽象语法树,又称AST)

    文章:https://juejin.im/post/5a9315e46fb9a0633a711f25 https://github.com/jamiebuilds/babel-handbook/blo ...

  9. 理解Babel是如何编译JS代码的及理解抽象语法树(AST)

    Babel是如何编译JS代码的及理解抽象语法树(AST) 1. Babel的作用是?   很多浏览器目前还不支持ES6的代码,但是我们可以通过Babel将ES6的代码转译成ES5代码,让所有的浏览器都 ...

随机推荐

  1. PAT甲级满分攻略|记一次考试经历

    一次考试经历 今天是"大雪",很冷. 来到隔壁的学校考试,记得上一次来河中医是两年前大一刚开学吧,那天晚上印象比较深刻,6个室友骑车到处闲逛.当时还不会Hello world. 很 ...

  2. Netty学习——服务器端代码和客户端代码 原理详解

    服务器端代码和客户端代码 原理详解:(用到的API) 0.Socket 连接服务器端的套接字 1.TcompactProtocol   协议层2.TFrameTransport   传输层3.THsh ...

  3. 【LiteOS】STM32F103-LiteOS移植教程(详细篇)【华为云技术分享】

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/devcloud/article/detai ...

  4. MongoDB的第二天(更新,删除,查询,索引)

    Mongodb的更新方式有三种 update函数,操作符更新,save函数 update: 语法格式:db.COLLECTION_NAME.update({查询条件},{更新内容},{更新参数(可选) ...

  5. 洛谷 题解 P1287 【盒子与球】

    题解:P1287 盒子与球 不了解的:stirling数(斯特林数) - 百度百科 分析如下: 设有n个不同的球,分别用b1,b2,--bn表示.从中取出一个球bn,bn的放法有以下两种: 1) bn ...

  6. python学习笔记—DataFrame和Series的排序

    更多大数据分析.建模等内容请关注公众号<bigdatamodeling> ################################### 排序 ################## ...

  7. 适用于Java开发人员的SOLID设计原则简介

    看看这篇针对Java开发人员的SOLID设计原则简介.抽丝剥茧,细说架构那些事——[优锐课] 当你刚接触软件工程时,这些原理和设计模式不容易理解或习惯.我们都遇到了问题,很难理解SOLID + DP的 ...

  8. checkbox多选框取值

    var SelectQuestionAnswer = $("input:checkbox[name='SelectQuestionAnswer']:checked").map(fu ...

  9. Happy Birthday! 今天我 1 周岁生日啦!

    2018.09.28,我第 1 天分享文章. 2019.09.28,我连续分享的第 365 天. 今天我 1 周岁啦! 生日意味着一个新的开端, 意味着重新把握生活的机会. 新的一岁,从新头像开始 愿 ...

  10. .NET Core的响应式框架,基于Ace Admin框架菜单导航,Bootstrap布局,fontAwesome图标,内嵌Iframe用EasyUI做数据绑定,动态配置列表,动态配置表单

    netnrf 响应式框架 用于快速开发的响应式框架 演示:https://rf2.netnr.com v3.x 前端采用 jQuery + Bootstrap + EasyUI + AceAdmin ...