第16期 - 外白渡桥

封面图来源于外白渡桥,一个看东方明珠特别的位置,人少较为清静,漫步苏州河畔,有一丝丝的惬意。

技术笔记-AST

AST 全称 abstract syntax tree (抽象语法树)。它是源代码语法结构的一种抽象表示。

AST 的使用场景

应用于浏览器引擎、打包工具、代码转译、静态分析和代码格式化等场景。

解析 JavaScript

浏览器 js 引擎(如 V8)拿到 js 的第一件事就是解析 js 生成 AST。随后进行解释执行,通过 AST js 引擎可以理解代码的结构并逐行解释执行。再编译优化执行,在生成 AST 后 js 引擎会进行各种代码优化,然后将优化后的 AST 转换为字节码或机器码执行。

webpack

解析代码:使用 AST 来解析代码的模块依赖关系,通过分析 AST,可以识别出模块的导入和导出语句。 代码分割和树摇(Tree Shaking):利用 AST 进行代码分割,按需加载代码。通过 AST 分析代码,删除未使用的部分,减小打包后的文件体积。 插件和 Loader:Webpack 的插件和 Loader 可以基于 AST 修改代码。如 Babel Loader 使用 Babel 转换 ES6+ 代码为兼容的 ES5 代码。

ESLint

代码检查和规范:使用 AST 进行静态代码分析,检测代码中潜在错误和不符合规范的部分

Prettier

根据预定义的规则和配置文件自动重新排版代码,从而使代码风格统一。无论是缩进、换行还是空格,Prettier 都会按照规范进行调整 通过解析代码生成 AST,然后基于 AST 重新排版代码,以确保代码的一致性和可读性。通过 AST,可以忽略原始代码的格式,仅关注代码的语法结构,生成标准化的代码格式。

AST 相关库

ts-morph
https://ts-morph.com/navigation/

babel解析生成ast
https://babeljs.io/docs/babel-parser
babel遍历ast
https://babeljs.io/docs/babel-traverse

实用的在线AST工具

https://ts-ast-viewer.com/#
https://astexplorer.net/
https://esprima.org/demo/parse.html#

AST 结构

AST(抽象语法树)只有一个顶级节点——Program,它代表了整个程序的语法结构,Program 节点是所有代码片段的根节点,从这个节点出发可以访问程序的所有部分。 在 Program 节点之下,AST 会根据代码的结构将不同的代码片段区分开来,作为 Program 节点的子节点来处理。AST 的子节点类型包括 函数声明(FunctionDeclaration)、变量声明(VariableDeclaration)、表达式(ExpressionStatement)、块语句(BlockStatement)等。

function greet(name) {
  let greeting = "Hello, " + name;
  console.log(greeting);
}
{
  "type": "Program",  // 根节点,表示整个程序
  "body": [
    {
      "type": "FunctionDeclaration",  // 函数声明
      "id": {
        "type": "Identifier",
        "name": "greet"  // 函数名为 greet
      },
      "params": [
        {
          "type": "Identifier",
          "name": "name"  // 函数参数名为 name
        }
      ],
      "body": {
        "type": "BlockStatement",  // 函数体为块语句
        "body": [
          {
            "type": "VariableDeclaration",  // 变量声明语句
            "declarations": [
              {
                "type": "VariableDeclarator",  // 变量声明器
                "id": {
                  "type": "Identifier",
                  "name": "greeting"  // 变量名为 greeting
                },
                "init": {
                  "type": "BinaryExpression",  // 二元表达式,表示字符串拼接
                  "operator": "+",
                  "left": {
                    "type": "Literal",
                    "value": "Hello, ",  // 左操作数为字符串 "Hello, "
                    "raw": "\"Hello, \""  // 原始字符串表示
                  },
                  "right": {
                    "type": "Identifier",
                    "name": "name"  // 右操作数为参数 name
                  }
                }
              }
            ],
            "kind": "let"  // 使用 let 关键字声明变量
          },
          {
            "type": "ExpressionStatement",  // 表达式语句
            "expression": {
              "type": "CallExpression",  // 调用表达式
              "callee": {
                "type": "MemberExpression",  // 成员表达式,调用 console.log 方法
                "computed": false,
                "object": {
                  "type": "Identifier",
                  "name": "console"  // 对象部分,名称为 console
                },
                "property": {
                  "type": "Identifier",
                  "name": "log"  // 属性部分,名称为 log
                }
              },
              "arguments": [
                {
                  "type": "Identifier",
                  "name": "greeting"  // 传递 greeting 变量作为参数
                }
              ]
            }
          }
        ]
      },
      "generator": false,
      "expression": false,
      "async": false
    }
  ],
  "sourceType": "script"  // 源代码类型为脚本
}

生成 AST 的过程

生成 AST(抽象语法树)的过程主要包括两个步骤:词法分析(Lexical Analysis)和语法分析(Syntax Analysis)。

词法分析

词法分析器也叫 scanner(扫描器),顾名思义就是扫描我们的代码,遍历每个字符,使用预先定义好的规则将每个字符转换成 token(词法单元)。

词法分析阶段我们需要生成 token 列表, 核心思想就是遍历代码字符串,然后将字符串归类生成 token。

let name = "张三";
[
    {
        "type": "Keyword",
        "value": "let"
    },
    {
        "type": "Identifier",
        "value": "name"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "String",
        "value": "\"张三\""
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

自定义类型变量 constants.js

// token 类型
const TokenTypes = {
  Keyword: "Keyword", // 关键字
  Identifier: "Identifier", // 变量名标识符
  Punctuator: "Punctuator",  // 标点
  String: "String" // 字符串
}
// AST 节点类型定义
const AST_Types = {
  Literal: "Literal", // 字面量 就是值 如 let a = 1 1就是Literal
  Identifier: "Identifier", // 标识符 如 let a = 1 a就是Identifier
  AssignmentExpression: "AssignmentExpression", // 表示赋值表达式的节点类型
  VariableDeclarator: "VariableDeclarator", // 表示变量声明中的一个具体变量
  VariableDeclaration: "VariableDeclaration", // 表示变量声明语句
  Program: "Program" // Program 是顶层节点,表示整个 JavaScript 程序的根节点。它包含了程序中所有的语句和声明。
}

module.exports = {
  TokenTypes,
  AST_Types
}

词法分析生成 token // tokenizer.js

// 拿到刚才定义的类型变量
const customTokens = require("./constants")
const {TokenTypes } = customTokens

// 定义字符匹配规则
const KEYWORD = /let/ // 匹配关键字
const PUNCTUATOR = /[\=;]/ // 匹配"="、";"
const WHITESPACE = /\s/ // 匹配空格
const LETTERS = /[a-z]/i // 匹配字符

function tokenizer(input) {
  const tokens = [] // token列表存储token,并最终返回
  let current = 0 // 标记遍历到字符串的什么位置

  // 用while循环遍历代码字符串,直到遍历完整个字符串
  while (current < input.length) {
    let char = input[current] // 暂存一下当前遍历到的字符

    // 判断当前字符串是否是关键字或变量名
    if (LETTERS.test(char)) {
      let value = ''
      // 遍历当前字符所连接的完整字符,存入 value
      while (LETTERS.test(char)) {
        value += char
        char = input[++current]
      }
      // 判断当前字符串是否是关键字
      if (KEYWORD.test(value)) {
        tokens.push({
           type: TokenTypes.Keyword, // 标记token类型
           value: value // 记录token值
        })
      }
      // 否则是变量名
      else {
         tokens.push({
           type: TokenTypes.Identifier, // 标记为字符类型
           value: value // 记录值
         })
       }
       continue
    }

语法分析

语法分析就是将遍历得到的 token 列表,根据语法规则将 token 关联起来,形成一棵树形结构,这棵树就是 AST。所以 AST 表示的是源代码的语法结构,树上的每个节点表示的是源代码中的一种结构。

先看结果:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "value": "张三",
            "raw": "\"张三\""
          }
        }
      ],
      "kind": "let" // 标识变量声明的类型
    }
  ],
  "sourceType": "script"
}

AST 生成

// 拿到刚才定义的token类型和AST类型
const customTokens = require("./constants")
const {TokenTypes, AST_Types } = customTokens

// 语法解析
function parser(tokens) { // 这里的tokens是指上面解析生成的tokens
  let current = 0
  function walk() {
      // 拿到解析生成的token
      const token = tokens[current]
      // 处理字符节点
      if (token.type === TokenTypes.String) {
        current++;
        // 返回一个新的 AST 结点
        return {
          type: AST_Types.Literal, // 赋予AST节点类型
          value: JSON.parse(token.value),
          row: token.value
        }
      }
      // 处理变量名
      if (token.type === TokenTypes.Identifier) {
      current++;
      return {
        type: AST_Types.Identifier,
        name: token.value,
      };
    }
    // 检查运算符关键字
    if (token.type === TokenTypes.Punctuator) {
      current++;
      if(/\=/.test(token.value)){ // 判断是否是=号, 这里 因为只是简单示例所以只有=的处理,实际情况更复杂些
        return {
          type: AST_Types.AssignmentExpression,
          operator: token.value
        }
      }
      // 忽略掉;号,不算入AST中
      else{
        return
      }
    }
    // 检查关键字
    // 处理赋值语句的时候存在递归调用。同时我们忽略了一些token的处理,比如"="、";"
    if ( token.type === TokenTypes.Keyword) {
      var value = token.value // 得到 'let'
      current++; // 紧跟声明语句的就是变量名,下一步walk就可以返回变量名
      const variable = walk() // 递归获取定义的变量 处理可能嵌套的结构
      current++ // 下一个应该是=号,略过不算入AST中
      const rightVar = walk()
      current++ // 下一个应该是;号,略过不算入AST中

      // 定义声明
      const declaration = {
        type: AST_Types.VariableDeclarator,
        id: variable, // 定义的变量
        init: rightVar // 赋予的值
      }
      // 定义要返回的节点
      return {
        type: AST_Types.VariableDeclaration,
        declarations: [declaration],
        kind: value,
      };
    }
    // 类型未知的结点,抛错误
    throw new TypeError(token.type);
    }
  // 创建 AST,定义根结点是一个类型为 `Program` 的结点。
  const ast = {
    type: AST_Types.Program,
    body: [],
    sourceType: "script"
  };
  // 遍历被解析字符执行 walk 函数,把结点值放入 ast.body 中。
  while (current < tokens.length) {
    ast.body.push(walk());
  }
  return ast;
}

module.exports = parser

AST 测试工具 https://esprima.org/demo/parse.html# 查看生成的 Token 和 AST

实现一个简单的 js 编译器

把 es6 的语法转换成 es5 的语法,其实就是 babel 干的事情

编译器一般分三步走: Parsing(解析):负责将代码解析然后生成 AST
Transformation(转换): 根据 AST 来对代码做一些增删改的操作
Code Generation (代码生成):根据转换后 AST 重新生成新的代码

Parsing(解析)上述 AST 解析已讲

Transformation(转换) 将 let name = “张三”; 转换为 var name = “张三”; 即需要将 let 替换成 var,AST 转换成如下形式:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "value": "张三",
            "raw": "\"张三\""
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

实现:首先我们需要一个可以遍历 ast 的 traverser(遍历器)ast 是一颗树形结构,采用深度优先进行遍历,再调用 traverser 生成新的 ast // traverser.js

// 获取AST节点类型
const constants = require("./constants")
const { AST_Types } = constants
// visitor 为自定义函数
function traverser(ast, visitor) {
  // 遍历解析 body 节点数组
  function traverseArray(array, parent) {
      array.forEach(function(child) {
        traverseNode(child, parent);
      });
    }
}
// 处理每个节点
function traverseNode(node, parent) {
   // 首先看看 visitor 中有没有对应 type 的处理函数。
    const method = visitor[node.type]
    // 如果有,调用处理方法 不同类型节点对应不同的方法 详见下文调用处
    if (method) {
      method(node, parent)
    }
    // 下面对每一个不同类型的结点分开处理。
    switch (node.type) {
      case AST_Types.Program: // 顶层的 Program 开始,body是数组所以调用traverseArray
        traverseArray(node.body, node)
        break
        // 以下类型不需要转换,直接退出
      case AST_Types.VariableDeclaration:
      case AST_Types.VariableDeclarator:
      case AST_Types.AssignmentExpression:
      case AST_Types.Identifier:
      case AST_Types.Literal:
        break
      // 同样,如果不能识别当前的结点,那么就抛出一个错误。
      default:
        throw new TypeError(node.type)
    }
  }
  // 触发遍历AST,根节点没有父节点所以这里传入null
  traverseNode(ast, null)
}
module.exports = traverser
// transformer.js
// 获取转换器和AST类型
const traverser = require("./traverser")
const constants = require("./constants")
const { AST_Types } = constants

// transformer接收 AST 作为参数
function transformer(ast) {
  // newAst用于存储新的AST
  const newAst = {
    type: AST_Types.Program,
    body: [],
    sourceType: "script"
  };
  // 保存当前修改过的节点
  ast._context = newAst.body
  // 将 AST 和 visitor 传入traverser中
  traverser(ast, {
    // 将let转换为var
    VariableDeclaration: function(node, parent) {
      const variableDeclaration = {
        type: AST_Types.VariableDeclaration,
        declarations: node.declarations,
        kind: "var"
      };
      // 把新的 VariableDeclaration 放入到 context 中。
      parent._context.push(variableDeclaration)
    }
  });
  // 最后返回新的AST
  return newAst
}

module.exports = transformer

Code Generation (代码生成)根据新的 AST 来重新生成代码

// 获取AST类型
const constants = require("./constants")
const { AST_Types } = constants

function codeGenerator(node) {
  // 处理不同类型的结点
  switch (node.type) {
  // 遍历 body 属性中的每一个结点并加入换行符号
  case AST_Types.Program:
      return node.body.map(codeGenerator)
        .join('\n')

    case AST_Types.VariableDeclaration: // 处理变量声明
      return (
        node.kind + ' ' + node.declarations.map(codeGenerator)
      )
    case AST_Types.VariableDeclarator: // 处理声明的变量和值
      return (
        codeGenerator(node.id) + ' = ' +
        codeGenerator(node.init)
      )
    case AST_Types.Identifier: // 变量名直接返回
      return node.name

    case AST_Types.Literal: // 字符串加上""、;返回
      return '"'+node.value+'"'+";"

    // 如果我们不能识别这个结点,那么抛出一个错误。
    default:
      throw new TypeError(node.type)
  }
}

module.exports = codeGenerator

完整的代码执行流

const transformer = require("./transformer")
const tokenizer = require("./tokenizer")
const parser = require("./parser")
const codeGenerator = require("./codeGenerator")

const tokens = tokenizer('let name = "张三";')
const ast = parser(tokens)
const newAst = transformer(ast)
const newCode = codeGenerator(newAst)

console.log(newCode) // var name = "张三";

附:AST 节点类型对照表

节点类型描述
Identifier变量、函数或属性的名称。
Literal数字、字符串、布尔值等常量值。
ExpressionStatement包含在语句中的表达式。
BinaryExpression包含二元操作符及其两个操作数的表达式。
UnaryExpression包含一元操作符及其操作数的表达式。
CallExpression函数调用表达式,包括函数和参数。
MemberExpression对象属性访问表达式,如 obj.prop
AssignmentExpression赋值表达式,如 variable = value
FunctionDeclaration函数声明节点,定义函数及其主体。
VariableDeclaration变量声明节点,定义变量及其初始值。
IfStatement条件语句,包含条件表达式和执行语句。
ForStatement循环语句,包含初始化、条件和迭代语句。
WhileStatementWhile 循环语句,包含条件和执行语句。
ReturnStatement返回语句,用于从函数中返回值。
ObjectExpression对象字面量表达式,表示 JavaScript 中的对象。
ArrayExpression数组字面量表达式,表示 JavaScript 中的数组。
NewExpression创建新对象实例的表达式,如 new Constructor()