资讯专栏INFORMATION COLUMN

AST 实战

asoren / 3190人阅读

摘要:本文除了介绍的一些基本概念外,更偏重实战,讲解如何利用它来对代码进行修改。二基本概念全称,也就是抽象语法树,它是将编程语言转换成机器语言的桥梁。四实战下面我们来详细看看如何对进行操作。

欢迎关注我的公众号睿Talk,获取我最新的文章:

一、前言

最近突然对 AST 产生了兴趣,深入了解后发现它的使用场景还真的不少,很多我们日常开发使用的工具都跟它息息相关,如 Babel、ESLint 和 Prettier 等。本文除了介绍 AST 的一些基本概念外,更偏重实战,讲解如何利用它来对代码进行修改。

二、基本概念

AST 全称 Abstract Syntax Tree,也就是抽象语法树,它是将编程语言转换成机器语言的桥梁。浏览器在解析 JS 的过程中,会根据 ECMAScript 标准将字符串进行分词,拆分为一个个语法单元。然后再遍历这些语法单元,进行语义分析,构造出 AST。最后再使用 JIT 编译器的全代码生成器,将 AST 转换为本地可执行的机器码。如下面一段代码:

</>复制代码

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

进行分词后,会得到这些 token:

对 token 进行分析,最终会得到这样一棵 AST(简化版):

</>复制代码

  1. {
  2. "type": "Program",
  3. "body": [
  4. {
  5. "type": "FunctionDeclaration",
  6. "id": {
  7. "type": "Identifier",
  8. "name": "add"
  9. },
  10. "params": [
  11. {
  12. "type": "Identifier",
  13. "name": "a"
  14. },
  15. {
  16. "type": "Identifier",
  17. "name": "b"
  18. }
  19. ],
  20. "body": {
  21. "type": "BlockStatement",
  22. "body": [
  23. {
  24. "type": "ReturnStatement",
  25. "argument": {
  26. "type": "BinaryExpression",
  27. "left": {
  28. "type": "Identifier",
  29. "name": "a"
  30. },
  31. "operator": "+",
  32. "right": {
  33. "type": "Identifier",
  34. "name": "b"
  35. }
  36. }
  37. }
  38. ]
  39. }
  40. }
  41. ],
  42. "sourceType": "module"
  43. }

拿到 AST 后就可以根据规则转换为机器码了,在此不再赘述。

三、Babel 工作原理

AST 除了可以转换为机器码外,还能做很多事情,如 Babel 就能通过分析 AST,将 ES6 的代码转换成 ES5。

Babel 的编译过程分为 3 个阶段:

解析:将代码字符串解析成抽象语法树

变换:对抽象语法树进行变换操作

生成:根据变换后的抽象语法树生成新的代码字符串

Babel 实现了一个 JS 版本的解析器Babel parser,它能将 JS 字符串转换为 JSON 结构的 AST。为了方便对这棵树进行遍历和变换操作,babel 又提供了traverse工具函数。完成 AST 的修改后,可以使用generator生成新的代码。

四、AST 实战

下面我们来详细看看如何对 AST 进行操作。先建好如下的代码模板:

</>复制代码

  1. import parser from "@babel/parser";
  2. import generator from "@babel/generator";
  3. import t from "@babel/types";
  4. import traverser from "@babel/traverse";
  5. const generate = generator.default;
  6. const traverse = traverser.default;
  7. const code = ``;
  8. const ast = parser.parse(code);
  9. // AST 变换
  10. const output = generate(ast, {}, code);
  11. console.log("Input
  12. ", code);
  13. console.log("Output
  14. ", output.code);

构造一个 hello world

打开 AST Explorer,将左侧代码清空,再输入 hello world,可以看到前后 AST 的样子:

</>复制代码

  1. // 空
  2. {
  3. "type": "Program",
  4. "body": [],
  5. "sourceType": "module"
  6. }
  7. // hello world
  8. {
  9. "type": "Program",
  10. "body": [
  11. {
  12. "type": "ExpressionStatement",
  13. "expression": {
  14. "type": "Literal",
  15. "value": "hello world",
  16. "raw": ""hello world""
  17. },
  18. "directive": "hello world"
  19. }
  20. ],
  21. "sourceType": "module"
  22. }

接下来通过代码构造这个ExpressionStatement:

</>复制代码

  1. const code = ``;
  2. const ast = parser.parse(code);
  3. // 生成 literal
  4. const literal = t.stringLiteral("hello world")
  5. // 生成 expressionStatement
  6. const exp = t.expressionStatement(literal)
  7. // 将表达式放入body中
  8. ast.program.body.push(exp)
  9. const output = generate(ast, {}, code);

可以看到 AST 的创建过程就是自底向上创建各种节点的过程。这里我们借助 babel 提供的types对象帮我们创建各种类型的节点。更多类型可以查阅这里。

同样道理,下面我们来看看如何构造一个赋值语句:

</>复制代码

  1. const code = ``;
  2. const ast = parser.parse(code);
  3. // 生成 identifier
  4. const id = t.identifier("str")
  5. // 生成 literal
  6. const literal = t.stringLiteral("hello world")
  7. // 生成 variableDeclarator
  8. const declarator = t.variableDeclarator(id, literal)
  9. // 生成 variableDeclaration
  10. const declaration = t.variableDeclaration("const", [declarator])
  11. // 将表达式放入body中
  12. ast.program.body.push(declaration)
  13. const output = generate(ast, {}, code);

获取 AST 中的节点

下面我们将对这段代码进行操作:

</>复制代码

  1. export default {
  2. data() {
  3. return {
  4. count: 0
  5. }
  6. },
  7. methods: {
  8. add() {
  9. ++this.count
  10. },
  11. minus() {
  12. --this.count
  13. }
  14. }
  15. }

假设我想获取这段代码中的data方法,可以直接这么访问:

</>复制代码

  1. const dataProperty = ast.program.body[0].declaration.properties[0]

也可以使用 babel 提供的traverse工具方法:

</>复制代码

  1. const code = `
  2. export default {
  3. data() {
  4. return {
  5. count: 0
  6. }
  7. },
  8. methods: {
  9. add() {
  10. ++this.count
  11. },
  12. minus() {
  13. --this.count
  14. }
  15. }
  16. }
  17. `;
  18. const ast = parser.parse(code, {sourceType: "module"});
  19. // const dataProperty = ast.program.body[0].declaration.properties[0]
  20. traverse(ast, {
  21. ObjectMethod(path) {
  22. if (path.node.key.name === "data") {
  23. path.node.key.name = "myData";
  24. // 停止遍历
  25. path.stop();
  26. }
  27. }
  28. })
  29. const output = generate(ast, {}, code);

traverse方法的第二个参数是一个对象,只要提供与节点类型同名的属性,就能获取到所有的这种类型的节点。通过path参数能访问到节点信息,进而找出需要操作的节点。上面的代码中,我们找到方法名为data的方法后,将其改名为myData,然后停止遍历,生成新的代码。

替换 AST 中的节点

可以使用replaceWithreplaceWithSourceString替换节点,例子如下:

</>复制代码

  1. //this.count 改成 this.data.count
  2. const code = `this.count`;
  3. const ast = parser.parse(code);
  4. traverse(ast, {
  5. MemberExpression(path) {
  6. if (
  7. t.isThisExpression(path.node.object) &&
  8. t.isIdentifier(path.node.property, {
  9. name: "count"
  10. })
  11. ) {
  12. //this 替换为 this.data
  13. path
  14. .get("object")
  15. .replaceWith(
  16. t.memberExpression(t.thisExpression(), t.identifier("data"))
  17. );
  18. // 下面的操作跟上一条语句等价,更加直观方便
  19. // path.get("object").replaceWithSourceString("this.data");
  20. }
  21. }
  22. });
  23. const output = generate(ast, {}, code);

插入新的节点

可以使用pushContainerinsertBeforeinsertAfter等方法来插入节点:

</>复制代码

  1. // 这个例子示范了 3 种节点插入的方法
  2. const code = `
  3. const obj = {
  4. count: 0,
  5. message: "hello world"
  6. }
  7. `;
  8. const ast = parser.parse(code);
  9. const property = t.objectProperty(
  10. t.identifier("new"),
  11. t.stringLiteral("new property")
  12. );
  13. traverse(ast, {
  14. ObjectExpression(path) {
  15. path.pushContainer("properties", property);
  16. // path.node.properties.push(property);
  17. }
  18. });
  19. /*
  20. traverse(ast, {
  21. ObjectProperty(path) {
  22. if (
  23. t.isIdentifier(path.node.key, {
  24. name: "message"
  25. })
  26. ) {
  27. path.insertAfter(property);
  28. }
  29. }
  30. });
  31. */
  32. const output = generate(ast, {}, code);

删除节点

使用remove方法来删除节点:

</>复制代码

  1. const code = `
  2. const obj = {
  3. count: 0,
  4. message: "hello world"
  5. }
  6. `;
  7. const ast = parser.parse(code);
  8. traverse(ast, {
  9. ObjectProperty(path) {
  10. if (
  11. t.isIdentifier(path.node.key, {
  12. name: "message"
  13. })
  14. ) {
  15. path.remove();
  16. }
  17. }
  18. });
  19. const output = generate(ast, {}, code);
五、总结

本文介绍了 AST 的一些基本概念,讲解了如何使用 Babel 提供的 API,对 AST 进行增删改查的操作。​掌握这项技能,再加上一点想象力,就能制作出实用的代码分析和转换工具。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105902.html

相关文章

  • Babylon-AST初探-实战

    摘要:生成属性这一步,我们要先提取原函数中的的对象。所以这里我们还是主要使用来访问节点获取第一级的,也就是函数体将合并的写法用生成生成生成插入到原函数下方删除原函数程序输出将中的属性提升一级这里遍历中的属性没有再采用,因为这里结构是固定的。   经过之前的三篇文章介绍,AST的CRUD都已经完成。下面主要通过vue转小程序过程中需要用到的部分关键技术来实战。 下面的例子的核心代码依然是最简单...

    godiscoder 评论0 收藏0
  • Babylon-AST初探-代码更新&删除(Update & Remove)

    摘要:操作通常配合来完成。因为是个数组,因此,我们可以直接使用数组操作自我毁灭方法极为简单,找到要删除的,执行就结束了。如上述代码,我们要删除属性,代码如下到目前为止,的我们都介绍完了,下面一篇文章以转小程序为例,我们来实战一波。   通过前两篇文章的介绍,大家已经了解了Create和Retrieve,我们接着介绍Update和 Remove操作。Update操作通常配合Create来完成。...

    levius 评论0 收藏0
  • AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解

    摘要:抽象语法树,是一个非常基础而重要的知识点,但国内的文档却几乎一片空白。事实上,在世界中,你可以认为抽象语法树是最底层。通过抽象语法树解析,我们可以像童年时拆解玩具一样,透视这台机器的运转,并且重新按着你的意愿来组装。 抽象语法树(AST),是一个非常基础而重要的知识点,但国内的文档却几乎一片空白。本文将带大家从底层了解AST,并且通过发布一个小型前端工具,来带大家了解AST的强大功能 ...

    godiscoder 评论0 收藏0
  • FE.SRC-Vue实战与原理笔记

    摘要:超出此时间则渲染错误组件。元素节点总共有种类型,为表示是普通元素,为表示是表达式,为表示是纯文本。 实战 - 插件 form-validate {{ error }} ...

    wangjuntytl 评论0 收藏0
  • Vue编程三部曲之模型树优化实战代码

      实践是所有展示最好的方法,因此我觉得可以不必十分细致的,但我们的展示却是整体的流程、输入和输出。现在我们就看看Vue 的指令、内置组件等。也就是第二篇,模型树优化。  分析了 Vue 编译三部曲的第一步,「如何将 template 编译成 AST ?」上一篇已经介绍,但我们还是来总结回顾下,parse 的目的是将开发者写的 template 模板字符串转换成抽象语法树 AST ,AST 就这里...

    3403771864 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<