AST抽象语法树的基础学习和理解

170 阅读4分钟

前言

面试的时候总会有不同的面试官喜欢问一句,你知道AST有了解吗,能具体描述下吗?这时候你就会想起那句面试造火箭,工具拧螺丝的世界名言,不由得想给这个面试官一锤让他醒醒。其实AST在我们平时写业务代码的时候极少用到,但是在webpack,脚手架编写,甚至是编辑器的插件(高亮插件等)中却经常会出现,你得弄懂这些概念才能理解如何去写一个插件。

概念

官方解释:在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。简单地说,,它是一棵树,用分支和节点的组合来描述代码结构。他可以让计算机理解我们写的代码

为了方便理解,可以通过一个例子去看看

let a = 'ronald'; //简单的赋值语句

我们可以通过AST转换网站去看看转换后的结构(AST Explorer

js源代码转换成AST去描述之后变成

{
  "type": "Program",
  "start": 0,
  "end": 17,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 17,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 16,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 16,
            "value": "ronald",
            "raw": "'ronald'"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}

通过上面我们可以看到一条语句其实是由若干个词法单元组成。每一个单词都有对应的词法语句去描述。

AST的用处

  • vue 模板编译、react 模板编译
  • webpack、rollup 进行代码打包等
  • JSLint、JSHint 对代码错误或风格的检查等
  • IDE 的错误提示、代码格式化、代码高亮、代码自动补全等

AST如何生成

AST 整个解析过程分为两个步骤(词法分析和语法分析)

  • 词法分析 (Lexical Analysis):词法组成语言的单词,是语言中最小单元,扫描输入的源代码字符串之后,生成一系列的词法单元(tokens)。这些词法单元包括数字,标点符号,运算符等。词法单元之间都是独立的,也即在该阶段我们并不关心每一行代码是通过什么方式组合在一起的。

用一段代码举例

const a = 10;
[
    { type: "KEYWORD_CONST", value: "const" },
    { type: "VARIABLE", value: "a" },
    { type: "OPERATOR_EQUAL", value: "=" }, 
    { type: "INTEGER", value: "10" }
...
]

  • 语法分析 (Syntax Analysis):语法,是词法之间的组合方式,语法分析的任务就是用由词法分析得到的令牌流,在上下文无关文法(一般指某种程序设计语言上的语法)的约束下,生成树形的中间表示(便于描述逻辑结构),该中间表示给出了令牌流的结构表示,同时验证语法,语法如果有错的话,抛出语法错误。

经过词法分析和语法分析之后就生成了AST,用一棵树形的数据结构来描述源代码。

通过实战例子去尝试使用AST来解决问题

  1. 尝试去掉方法里面的debugger;
function drop(){
    console.log("123");
    debugger;
}

我们可以先在(AST Explorer)中对js源码进行AST分析,看看具体的AST结构

{
  "type": "Program",
  "start": 0,
  "end": 49,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 49,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 13,
        "name": "drop"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 15,
        "end": 49,
        "body": [
          {
            "type": "ExpressionStatement",
            "start": 18,
            "end": 35,
            "expression": {
              "type": "CallExpression",
              "start": 18,
              "end": 34,
              "callee": {
                "type": "MemberExpression",
                "start": 18,
                "end": 29,
                "object": {
                  "type": "Identifier",
                  "start": 18,
                  "end": 25,
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "start": 26,
                  "end": 29,
                  "name": "log"
                },
                "computed": false,
                "optional": false
              },
              "arguments": [
                {
                  "type": "Literal",
                  "start": 30,
                  "end": 33,
                  "value": 123,
                  "raw": "123"
                }
              ],
              "optional": false
            }
          },
          {
            "type": "DebuggerStatement",
            "start": 38,
            "end": 47
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

通过上面可以比较清晰的能看到,debugger存在于type=DebuggerStatement中,那我们就可以通过遍历这个AST上的节点,找到type=DebuggerStatement的节点,然后删除就可以实现这个功能了。

找到解决思路之后,就可以使用工具了。我们可以使用 @babel/parser、@babel/traverse、@babel/generator这三个工具,他们分别的作用是解析、转换、生成

const parser = require('@babel/parser');  //用于转换成AST
const traverse = require("@babel/traverse"); //用于遍历、更新节点的操作
const generator = require("@babel/generator"); //用于生成最终的js语句
const types = require("@babel/types");// 主要用于判断类型

// 源代码
const code = `
function drop() {
  console.log('123')
  debugger;
}
`;

// 1. 源代码解析成 ast
const ast = parser.parse(code);


// 2. 转换  使用来遍历和更新节点,同时也提供了很多特定的节点类型方法,具体可以查@babel/trav
const tranfer = {
  //专门查找debugger的类型方法
  DebuggerStatement(path) {
    // 找到后删除该抽象语法树节点
    path.remove();
  }
}
traverse.default(ast, tranfer);

// 3. 生成
const result = generator.default(ast, {}, code);

console.log(result.code)

// 4. 日志输出

// function fn() {
//   console.log('123');
// }

//可以看到,方法里面的debugger已经被消灭掉了。

以上就是我理解的最基本的AST了。我猜如果能答到这一点,应该面试官就不会深问下去了吧,毕竟AST用到的地方确实是比较少,理解其中的原理即可,技术菜求轻拍。