是时候好好认识下AST这个熟悉而又陌生的朋友了~

757 阅读6分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

最近在捣鼓自己的项目时,遇到了AST这个似曾相识的家伙。熟悉是因为我大概知道它是我们写的代码的一种抽象表示,陌生是因为自己对它的了解也仅限于此,所以我趁着这个机会对AST好好研究了一番,收获还是蛮多的,所以决定整理成文分享出来,希望能让它也成为你熟悉的伙伴

初识AST

AST(abstract tree)全称 抽象语法树,见名知其义,AST就是对我们所写源码的一种抽象表示,表示的形式就是树形结构,它与我们熟悉的 virtual dom 的底层逻辑是一致的:为了能更方便地对目标(dom,source code)进行操作(新增,删除,查询,更新),借助增加一层抽象层(VDOM,AST)的方式来达到目的

由此可见,AST的出现不是目的,而是手段,这个目的在不同的工具下会有所不同,下面以部分常见工具为例进行说明

  • Babel,借助AST将js高版本语法转换为我们设定的较低版本的语法,从而可以让我们提前用上目标浏览器还未支持的js新特性
  • Uglify,借助AST将js代码中的注释,空白符删除掉,从而实现压缩代码的目的
  • Eslint,借助AST分析代码中存在的问题,从而可以反馈给开发者
  • 编辑器,借助AST实现实时分析用户输入的代码,从而实现代码高亮智能提示语法错误提示等功能
  • V8引擎,AST在V8引擎执行js的过程中起着承上启下的作用,其大致分为以下三步,
    • 借助解析器(Parser)将js源码解析成AST
    • 借助解释器(Ignition)将AST编译为字节码文件
    • 在代码真正被执行时,借助解释器将字节码编译为机器码,这一步也解释了为什么js是解释型的语言,因为从字节码到机器码这一步是实时编译的,也就是一边编译一边执行。

AST虽然在我们的日常开发中不会直接接触,但是它其实存在于开发过程中的各个环节里,默默地为我们撑起了前端的一片天

niubi.jpg

因此,打好AST这个地基也是我们急需完成的事情,因为只有地基稳了,我们的上层建筑才能稳定且持续发展,所以接下来让我们深入对它研究一番吧

生成AST的过程

在经过上文对AST的描述后,你肯定会有疑问:我们所写的源码是怎么转换为一棵抽象语法树的呢? 是的,我一开始也跟你有一样的疑问并觉得这个过程很'黑魔法',因为能把两个看似完全不一样的东西进行转化,想想都很神奇

在经过对AST的研究后,我已经解开了心中的疑问,接下来就让我为你解开心中疑问吧🤪。

niubi2.jpg

AST的生成分为两步:

词法分析

词法分析也称为扫描,在词法分析阶段,会一个一个字符读取源码,然后与js中有特定含义的字符串进行比对,从而生成很多个Token(Token是一个不可分割的最小单元),在词法分析器里,每个关键字是一个Token,每个标识符是一个Token,每个操作符是一个Token,每个标点符号也是一个Token,例如 import 就是一个token,它属于关键字并且不能再被切分,否则就会失去原本的语义

需要注意的是词法分析会过滤掉源代码中的注释和空白字符(换行符、空格、制表符等),这也就是说明AST并不能完全表示源代码,而如果要完全表示源代码,是需要另一种树形结构,那便是CST(具体语法树),这里就不展开讲了

最终,整个源代码将被分割进一个Tokens列表(或者说一维数组),下面给出一个示例

/* 源代码 */
function a(){
  return n * n
}

/* Tokens列表 */
[
  { type: { ... }, value: "function",  loc: { ... } },
  { type: { ... }, value: "a",  loc: { ... } },
  { type: { ... }, value: "(",  loc: { ... } },
  { type: { ... }, value: ")",  loc: { ... } },
  { type: { ... }, value: "{",  loc: { ... } },
  { type: { ... }, value: "return",  loc: { ... } },
  { type: { ... }, value: "n",  loc: { ... } },
  { type: { ... }, value: "*",  loc: { ... } },
  { type: { ... }, value: "n",  loc: { ... } },
  { type: { ... }, value: "}",  loc: { ... } }
]

type属性里有一组属性来描述该令牌:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

语法分析

在经过词法分析后,我们得到了一个Tokens列表,语法分析则是对这个Tokens列表进行转化,生成对应的AST,同时还会验证语法,如果存在错误,则会抛出语法错误,下面还是以上述例子中的源码为例,来看看最终生成的AST到底长啥样

/* 源代码 */
function a(){
  return n * n
}

/* AST */
{
  "type": "Program",
  "start": 0,
  "end": 30,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 30,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 10,
        "name": "a"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 12,
        "end": 30,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 16,
            "end": 28,
            "argument": {
              "type": "BinaryExpression",
              "start": 23,
              "end": 28,
              "left": {
                "type": "Identifier",
                "start": 23,
                "end": 24,
                "name": "n"
              },
              "operator": "*",
              "right": {
                "type": "Identifier",
                "start": 27,
                "end": 28,
                "name": "n"
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

可以看到虽然源代码很少,但是生成的AST却包含很多信息,细心观察可以发现,每一层都具有相似的结构

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}

{
  type: "Identifier",
  name: ...
}

{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

这些都可以看成是AST的一个节点(Node),一棵AST可以包含一个或多个节点,它们组合在一起可以描述用于静态分析的程序语法,webpack中的 tree-shaking 能力也正是来源于此

看到这里你又会产生疑问了:为什么AST一定要是这个结构,而不是其他结构呢?

dog.jpg

其实这里的AST结构是社区规范 EStree 所定义的,里面包含了各种节点类型的定义,有空了可以多瞅瞅,但是并不是所有解析器都遵循了这个规范,有一些会根据这个规范进行一定程度的调整来满足自身的需要,据我所知,完全遵循规范的解析器有:Accorn、Esprima、Espree

总结

AST的威力其实是非常强大的,我们可以通过它做许多非常有意思的事情,常见的如编写babel和webpack插件,不常见的就留给你自行脑洞🤪

最后推荐一个在线解析AST的网站:astexplorer,功能非常强大,可以转换各种类型的语言或者使用各种类型的解析器,可以根据自己的需要进行选择,好啦,就写到这里啦,完结,撒花🎉~

一点小小的请求

既然都看到这里啦,如果你喜欢我的文章,那么请动动你的手指,帮我的文章点个赞或收个藏,xdm的支持是我创作的最大动力,自己单机真不好玩!

最近自己搭建了个人博客,上面会最先发布我写的文章,希望感兴趣的小伙伴都去逛逛,如果能评论留言就更好啦,嘿嘿,期待你们的光临哦~