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

揭开,神秘,javascript,ast,面纱,基础,功能 · 浏览次数 : 179

小编点评

生成内容时需要带简单的排版,以下内容可以帮助你生成代码时带简单的排版: 1. **使用代码注释**: * 在你的代码中使用代码注释,例如 `//` 或 `/* */` ,可以帮助你快速生成代码时带简单的排版。 2. **使用代码模块**: * 将你的代码模块化,可以帮助你快速生成代码时带简单的排版。 3. **使用代码混淆**: * 将你的代码混淆,可以帮助你快速生成代码时带 simple 的排版。 4. **使用代码模块化**: * 将你的代码模块化,可以帮助你快速生成代码时带简单的排版。 5. **使用代码生成工具**: * 使用代码生成工具,可以帮助你快速生成代码时带简单的排版。

正文

作者:京东科技 周明亮

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))写入。

现在,我们就可以掌握新的方法,进行代码改造。

与带你揭开神秘的javascript AST面纱之AST 基础与功能相似的内容:

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

在前端里面有一个很重要的概念,也是最原子化的内容,就是 AST ,几乎所有的框架,都是基于 AST 进行改造运行,比如:React / Vue /Taro 等等。 多端的运行使用,都离不开 AST 这个概念。在大家理解相关原理和背景后,我们可以通过手写简单的编译器,简单实现一个 Javascript 的代码编译器,编译后在浏览器端正常运行。

带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法

对 AST 有了初步的认识,还有常规的代码改造应用实践,现在我们来详细说说使用 AST, 如何进行代码改造?

手把手带你使用JWT实现单点登录

JWT(英文全名:JSON Web Token)是目前最流行的跨域身份验证解决方案之一,今天我们一起来揭开它神秘的面纱! 一、故事起源 说起 JWT,我们先来谈一谈基于传统session认证的方案以及瓶颈。 传统session交互流程,如下图: 当浏览器向服务器发送登录请求时,验证通过之后,会将用户

从数据链路到神秘的MAC地址和ARP协议

在当今互联世界中,数据的传输和通信是不可或缺的。然而,你是否曾想过,在网络通信中隐藏着哪些神秘的秘密?本文将带你深入探索数据链路层、MAC地址和ARP协议的奥秘。数据链路层是网络通信中的关键一环,负责将数据包封装为帧并进行传输。而MAC地址作为设备的唯一标识符,扮演着识别节点的重要角色。而ARP协议则解决了从IP地址到MAC地址的映射问题,确保数据的准确传输。通过揭开这些网络通信的神秘面纱,你将更

IP协议:连接你我,掌握互联网的关键

IP协议作为互联网的基石,扮演着连接世界各个角落的重要角色。本文将带您深入了解IP协议的作用和位置,以及IP地址的重要性。无论您是对技术感兴趣的读者还是想了解互联网运作原理的人,本文都将为您揭开IP协议的神秘面纱,让您更好地理解数字世界的奇妙之处。

一篇带你了解如何使用纯前端类Excel表格构建现金流量表

现金流量表(Cash Flow Statement),是指反映企业在一定会计期间现金和现金等价物流入和流出的报表。现金流量表是企业财务报表的三个基本报告之一(另外两个是资产负债表和损益表)。 为了全面系统地揭示企业一定时期的财务状况、经营成果和现金流量,财务报表需按财政部会计准则的标准格式设计,因此

10个安全问题带你了解OWASP 定义的大模型应用

摘要:OWASP 的一群研究人员,总结目前大模型中可能存在的TOP10安全风险,很好的揭示了我们在大模型应用中需要防护的目标,以及如何采取相应的防护措施。 本文分享自华为云社区《OWASP 定义的大模型应用最常见的10个关键安全问题》,作者:Uncle_Tom。 1. OWASP Top 10 fo

还不知道如何在java中终止一个线程?快来,一文给你揭秘

简介 工作中我们经常会用到线程,一般情况下我们让线程执行就完事了,那么你们有没有想过如何去终止一个正在运行的线程呢? 今天带大家一起来看看。 Thread.stop被禁用之谜 问道怎么终止一个线程,可能大多数人都知道可以调用Thread.stop方法。 但是这个方法从jdk1.2之后就不推荐使用了,

[kernel] 带着问题看源码 —— 进程 ID 是如何分配的

Linux 可用 pid 上限是多少?如何提升上限?为何提升上限可以实时生效?Linux 底层如何实现 pid 快速分配与归还?这种实现为何只需要极少的内存开销?本文通过阅读 Linux 内核源码,一一为你揭秘

带你彻底搞懂递归时间复杂度的Master公式

网上找到的Master公式推导过程都太过于复杂了,为此我特地找到一种小白也能看懂的推导过程。看完这篇文章后,你会对递归的时间复杂度深谙于心,打死都不会忘记。