乐趣区

关于javascript:带你揭开神秘的javascript-AST面纱之AST-基础与功能

作者:京东科技 周亮堂

AST 根底与性能

在前端外面有一个很重要的概念,也是最原子化的内容,就是 AST,简直所有的框架,都是基于 AST 进行革新运行,比方:React / Vue /Taro 等等。多端的运行应用,都离不开 AST 这个概念。

在大家了解相干原理和背景后,咱们能够通过手写简略的编译器,简略实现一个 Javascript 的代码编译器,编译后在浏览器端失常运行。

 创立数字小明,等于六加一。创立数字小亮,等于七减二。输入,小明乘小亮。

通过实现一个自定义的编译器,咱们发现咱们本人也能写出很多新的框架。最终目标都是通过编译转换,翻译为浏览器辨认的 Javascript + CSS + HTML。

没错!翻译翻译~

当然咱们也能够以这个为根底,去实现跨端的框架,间接翻译为机器码,跑到各种硬件上。当然一个人必定比拟艰难,你会遇到各种各样的问题须要解决,不过没关系,只有你有好的想法,拉上一群人,你就能实现。

大家记得点赞,评论,珍藏,一键三连啊~

分析器

说到这个代码语义化操作前,咱们先说说分析器,其实就是编译原理。当你写了一段代码,要想让机器晓得,你写了啥。

那机器必定是要开始扫描,扫描每一个关键词,每一个符号,咱们将进行词法剖析的程序或者函数叫作词法分析器(Lexical analyzer),通过它的扫描能够将字符序列转换为单词(Token)序列的过程。

扫描到了关键词,咱们怎么能力把它依照规定,转换为机器意识的特定规定呢?比方你扫描到:

const a = 1

机器怎么晓得要创立一个 变量 a 并且等于 1 呢?

所以,这时候就引入一个概念:语法分析器(Syntactic analysis,Parser)。通过语法分析器,一直的调用词法分析器,进行语法查看、并构建由输出的单词组成的数据结构(个别是语法分析树、形象语法树等层次化的数据结构)。

在 JS 的世界里,这个扫描后失去的数据结构形象语法树【AST】。可能很多人听过这个概念,然而具体没有深刻理解。机缘巧合,刚好我须要用到这个玩意,明天就简略聊聊。

形象语法树 AST

AST 是 Abstract Syntax Tree 的缩写,也就是:形象语法树。在代码的世界里,它叫这个。在语言的世界外面,他叫语法分析树。

语言世界,举个栗子:

 我写文章。

语法分析树:
主语:我,人称代词。
谓语:写,动词。
宾语:文章,名词。

长一点的可能会有:主谓宾定状补。是不是发现好相熟,想当年大家学语文和英语,那是肯定要进行语法分析,不便你了解句子要表白的含意。

PS:对我来说,语法老难了!!!哈哈哈,大家是不是找到感觉了~

接下来咱们讲讲代码外面的形象语法树。

const me = "我"
function write() {console.log("文章")
}

那咱们用来进行语法分析,可能失去什么内容了?这时候咱们能够借助已有的工具,将他们进行剖析,进行一个高级入门。

其实咱们也能够齐全本人进行剖析,不过这样就不容易入门,定义的语法规定很多,如果只是看,很容易就被劝退了。而通过辅助工具,咱们能够很快承受相干的概念。

罕用的工具有很多,比方:Recast、Babel、Acorn 等等

也能够应用在线 AST 解析:AST Explorer,左上角菜单能够切换到各种解析工具,并且反对各类编程语言的解析,弱小好用,能够用来学习,帮忙你了解 AST。

为了帮忙大家了解,咱们一点点的进行解析,并且去掉了局部属性,留下骨干局部,残缺的能够通过在线工具查看。【 不同解析器,对于根节点或者局部属性稍有区别,然而实质是一样的。

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "me"
          },
          "init": {
            "type": "Literal",
            "value": "我",
            "raw": "\" 我 \""
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "FunctionDeclaration",
      "id": {
        "type": "Identifier",
        "name": "write"
      },
      "params": [],
      "body": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ExpressionStatement",
            "expression": {
              "type": "CallExpression",
              "callee": {
                "type": "MemberExpression",
                "object": {
                  "type": "Identifier",
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "name": "log"
                }
              },
              "arguments": [
                {
                  "type": "Literal",
                  "value": "文章",
                  "raw": "\" 文章 \""
                }
              ]
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

接下来,咱们一个一个节点看,首先是第一个节点 Program

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "kind": "const"
      ...
    },
    {
      "type": "FunctionDeclaration",
      "id": {
        "type": "Identifier",
        "name": "write"
      },
      ....
    }
  ],
  "sourceType": "module"
}

Program 是代码程序的根节点,通过它进行节点一层一层的遍历操作。下面咱们看出它有两个节点,一个是变量申明节点,另外一个是函数申明节点。

如果咱们再定义一个变量或者函数,这时候 body 就又会产生一个节点。咱们要扫描代码文件时,咱们就是基于 body 进行层层的节点扫描,直到把所有的节点扫描实现。

    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "me"
          },
          "init": {
            "type": "Literal",
            "value": "我",
            "raw": "\" 我 \""
          }
        }
      ],
      "kind": "const"
    },

下面对应的代码,就是 const me = “ 我 ”,这个节点通知咱们。申明一个变量,应用类型是:VariableDeclaration, 他的惟一标识名是:me,初始化值:” 我 ”。

后续的函数剖析,也是一样的。

{
      "type": "FunctionDeclaration",
      "id": {
        "type": "Identifier",
        "name": "write"
      },
      "params": [],
      "body": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ExpressionStatement",
            "expression": {
              "type": "CallExpression",
              "callee": {
                "type": "MemberExpression",
                "object": {
                  "type": "Identifier",
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "name": "log"
                },
              },
              "arguments": [
                {
                  "type": "Literal",
                  "value": "文章",
                  "raw": "\" 文章 \""
                }
              ],
            }
          }
        ]
      }
    }

这个节点,分明的通知咱们,这个函数名是什么,他外面有哪些内容,入参是什么,调用了什么函数对象。

咱们发现, 通过语法分析器的解析,咱们能够把代码,变成一个对象。这个对象将代码宰割为原子化的内容,很容易可能帮忙机器或者咱们去了解它的组成。

这个就是分析器的作用,咱们不再是一大段一大段的看代码逻辑,而是一小段一小段的看节点。

有了这个咱们能够干什么呢?

AST 在 JS 中的用处

1. 自定义语法分析器,写一个新的框架。

通过对现有的 AST 了解,咱们能够依葫芦画瓢,写出自定义的语法分析器,转成自定义的形象语法树,再进行解析转为浏览器可辨认的 Javascript 语言,或者其余硬件上能辨认的语言。

比方:React / Vue 等等框架。其实这些框架,就是自定义了一套语法分析器,用他们特定的语言,进行转换,翻译翻译,生成相干的 DOM 节点,操作函数等等 JS 函数。

2. 利用已有语法分析器,实现多端运行。

通过已有的 AST,咱们将代码进行翻译翻译,实现跨平台多端运行。咱们将失去代码进行语法解析,通过遍历所有的节点,咱们将他们进行革新,使得它可能运行在其余的平台上。

比方:Taro / uni-app 等等框架。咱们只有写一次代码,框架通过剖析转换,就能够运行到 H5 / 小程序等等相干的客户端。

3. 进行代码革新,预编译加强解决。

仍旧是通过已有的 AST,咱们将代码进行剖析。再进行代码混同,代码模块化解决,主动进行模块引入,低版本兼容解决。

比方:Webpack / Vite 等等打包工具。咱们写完代码,通过他们的解决,进行加强编译,加强代码的健壮性。

AST 的利用实际

咱们在进行框架的革新或者适配时,咱们可能才会用到这个。惯例的办法,可能有两种:

  • 依照特定的写法,通过正则表达式,间接进行大段代码替换。
  • /** mingliang start /consta=1/ mingliang end /

如,咱们找到这段代码正文,间接通过 code.replace(/mingliang/g, ‘xxxx’) 相似这种形式替换。

  • 通过引入运行,革新相干的变量,再从新写入。
// a.js
cost config = {a: 1}
return config

咱们可能先 let config = require(a.js) 运行这个文件,咱们就失去了这个 config 这个变量值。

之后咱们改写变量 config.a = 2,

最初,从新通过 fs.writeSync(‘a.js’, ‘return ‘ + JSON.stringify(config, null, 2)) 写入。

当初,咱们就能够把握新的办法,进行代码革新。

退出移动版