初识ast

718 阅读2分钟

抽象语法树(Abstract Syntax Tree)是实现js转译、CSS 预处理、代码压缩、ESLint、Prettier等的基石

AST是什么

是一种源代码的抽象结构的树形表示。树中的每个节点都表示源代码中出现的一个构造

AST的生成

词法分析

在词法分析阶段扫描输入的源代码字符串,生成一系列的词法单元,词法单元包括数字,标点符号,运算符等,且词法单元相互独立。(对代码语句进行拆分,不用考虑相互之间的联系)

Code: Hello('krystal') 
--->
Tokenization 
Token:[ 
        { type:'Identifer',value:'Hello' }, 
        { type:'Punctuator',value:'{' }, 
        { type:'String',value:'krystal' }, 
        { type:'Punctuator',value:'}' } 
     ]

语法分析

语法分析会根据生成的token列表转换为AST,AST代表了代码语句中每一个片段以及他们之间的联系。AST是一个嵌套很深的对象,代表了代码本身,也能给到我们更多关于代码的信息。

Token:
    [ 
      { type:'Identifer',value:'Hello' }, 
      { type:'Punctuator',value:'{' }, 
      { type:'String',value:'krystal' }, 
      { type:'Punctuator',value:'}' }
    ] 
    ---> AST:{ 
        "program"{ 
            "type": "Program", 
            "body": { 
            "type":"ExpressionStatement", 
            "expression": { 
                "type": "CallExpression", //调用表达式 
                "callee": { 
                "type":"Identifier", 
                "name": "Hello" 
                }, 
                "arguments": { 
                "type": "StringLiteral" 
                "value": "krystal" 
                } 
              } 
            } 
          } 
        }

代码生成

首先需要经历转换,生成新的AST,这个阶段仅仅是将上个阶段生成的AST进行修改。转换AST的过程中可以对节点进行添加、移除、替换等操作,仅仅是在旧有的AST基础上创建一个全新的AST

old AST ---> new AST:{ 
    "program":{ 
        "type":"Program", 
        "body":{ 
            "type": "CallExpression", 
            "name": "Hello", 
            "params": { 
                "type": "StringLiteral", 
                "value": "krystal" 
                } 
              } 
            } 
          }

接下来经历的是对AST树的遍历,遍历的过程会议DFS的方式到达每个节点。

  • Program -从AST的顶层开始
  • CallExpression -移动到program列表的第一个元素
  • CallExpression(Hello) -移动到callExpression的params列表的第一个元素

针对这里的对树的遍历,会产生一个访问的概念,也就是一步一步的对对象结构的元素进行 操作。需要创建一个访问器对象,来提供接受不同节点类型的方法。

根据我举的例子,仅涉及到StringLiteral,CallExpression。每当遇到对应的匹配的节点时,便会调用访问器对应的方法。

var visitor = { StringLiteral(node,parent) {} CallExpression(node, parent) {} }

访问的同时也需要存在退出的可能性,所以根据所举的例子,最终的访问流程是这样的

-> Program(enter) 
    ->CallExpression(enter) 
        ->StringLiteral(enter) 
        <-StringLiteral(exit) 
    <-CallExpression(exit) 
<- Program(exit)

最终的访问器应该设计成以下形式

var visitor = { 
  StringLiteral: { 
    enter(node,parent){} 
    exit(node,parent){} 
  } 
  CallExpression: { 
    enter(node,parent){} 
    exit(node,parent){} } 
  }

生成代码

代码生成器会递归调用AST对象上的所有嵌套节点,直到所有内容都被打印到一个常常的代码字符串中。

AST的基本结构

js编译器的通用规范 --ESTree中对于AST结构的基本定义

AST.png

AST的使用场景

  • 代码高亮、格式化、错误提示、自动补全
  • 代码压缩: uglifyJS
  • 代码转译: webpack、babel、TS