整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript - 什么是抽象语法树(AST)

象语法树(Abstract Syntax Trees),简称AST,如果您正在编写代码,那么 AST 很可能已经参与了您的开发流程。它们为您的开发流程的许多部分提供动力。 有些人可能在编译器的上下文中听说过它们,但它们被用于各种工具中。 即使您不编写通用开发工具,AST 也可能是您工具带中的有用工具。 在这篇文章中,我们将讨论什么是 AST,它们在哪里使用以及如何利用它们。

什么是AST

抽象语法树或 AST 是代码的树型数据结构表示。 它们是编译器工作方式的基本部分。 当编译器转换某些代码时,基本上有以下步骤:

  1. 词法分析
  2. 语法分析
  3. 代码生成

词法分析又名标记化

在此步骤中,您编写的代码将被转换为一组描述代码不同部分的标记。 这与基本语法突出显示使用的方法基本相同。 这些标记令牌不了解事物如何组合在一起,并且仅关注文件的组件。

你可以想象这就像你将一个文本分解成单词。 您可能能够区分标点符号、动词、名词、数字等,但在这个阶段,您对句子的组成部分或句子如何组合没有任何更深入的了解。

语法分析又名解析

这是我们将标记列表转换为抽象语法树的步骤。 它将我们的标记转换为表示代码实际结构的树。 以前在标记中我们只有一对 (),现在我们知道它是函数调用、函数定义、分组还是其他东西。

这里的等价物是将我们的单词列表转换为表示诸如句子之类的数据结构,某个名词在句子中扮演什么角色,或者我们是否在列表中。

另一个可以与之比较的例子是 DOM。 上一步只是将 HTML 分解为“标签”和“文本”,而这一步将生成表示为 DOM 树的层次结构。

需要注意的一件事是没有“单一”的 AST 格式。 它们可能会有所不同,这取决于您要转换为 AST 的语言以及您用于解析的工具。 在 JavaScript 中,一个通用标准是 ESTree,但您会看到不同的工具可能会添加不同的属性。

一般来说,AST 是一种树结构,其中每个节点至少有一个类型来指定它所代表的内容。

代码生成

此步骤本身可以是多个步骤。 一旦我们有了抽象语法树,我们就可以操作它,也可以将它“打印”到不同类型的代码中。 使用 AST 操作代码比直接在代码上作为文本或标记列表执行这些操作更安全。

操纵文本总是很危险的; 它显示最少的上下文。 如果您曾经尝试使用字符串替换或正则表达式来操作文本,您可能会注意到很容易出错。而且不容易调试。

甚至操纵令牌也不容易。 虽然我们可能知道变量是什么,但如果我们想重命名它,我们将无法深入了解变量的范围或可能与之冲突的任何变量。

AST 提供了有关代码结构的足够信息,我们可以更有信心地对其进行修改。 例如,我们可以确定变量的声明位置,并确切地知道由于树结构而影响程序的哪个部分。

一旦我们操纵了树,我们就可以打印树以输出任何预期的代码输出。 例如,如果我们要构建一个像 TypeScript 编译器这样的编译器,我们会输出 JavaScript,而另一个编译器可能会输出机器代码。

同样,使用 AST 更容易实现这一点,因为相同结构的不同输出可能具有不同的格式。 使用更线性的输入(如文本或标记列表)生成输出会相当困难。

如何处理 AST?

理论涵盖了哪些实际生活中的 AST 用例? 我们讨论了编译器,但我们并不是整天都在构建编译器。

AST 的用例很广泛,通常可以分为三个总体操作:读取、修改和打印。 它们是一种添加剂,这意味着如果您正在打印 AST,那么您以前也阅读过 AST 并对其进行修改的可能性很高。 但我们将介绍每个主要关注一个用例的示例。

读取/遍历 AST

从技术上讲,使用 AST 的第一步是解析文本以创建 AST,但在大多数情况下,提供解析步骤的库也提供了一种遍历 AST 的方法。遍历 AST 意味着访问树的不同节点以获取细节或执行操作。

最常见的用例之一是 linting。 例如,ESLint 使用 espree 生成 AST,如果您想编写任何自定义规则,您将根据不同的 AST 节点编写这些规则。 ESLint 文档有大量关于如何构建自定义规则、插件和格式化程序的文档。

修改/转换 AST

如前所述,与将代码修改为标记或原始字符串相比,拥有 AST 使修改所述树更容易、更安全。您可能想要使用 AST 修改某些代码的原因有很多种。

例如,Babel 修改 AST 以向下编译新功能或将 JSX 转换为函数调用。例如,当您编译 React 或 Preact 代码时会发生这种情况。

另一个用例是捆绑代码。在模块的世界中,捆绑代码通常比将文件附加在一起要复杂得多。更好地了解各个文件的结构可以更轻松地合并这些文件并在必要时调整导入和函数调用。如果您查看 webpack、parcel 或 rollup 等工具的代码库,您会发现它们都使用 AST 作为其捆绑工作流程的一部分。

打印 AST

在大多数情况下,打印和修改 AST 是齐头并进的,因为您必须输出刚刚修改的 AST。 但是,虽然像 recast 这样的一些库明确专注于以与原始代码样式相同的代码样式打印 AST,但也有各种用例,您希望以不同的方式显式打印您的 AST。

例如,Prettier 使用 AST 根据您的配置重新格式化您的代码,而无需更改代码的内容/含义。 他们这样做的方式是将您的代码转换为完全与格式无关的 AST,然后根据您的规则重写它。

其他常见的用例是用不同的目标语言打印代码或构建自己的缩小工具。

您可以使用几种不同的工具来打印 AST,例如 escodegen 或 astring。 您还可以根据您的用例构建自己的格式化程序,或者为 Prettier 构建一个插件。

最后:

虽然 AST 可能是大多数开发人员每天都不会使用的东西,但我相信了解它对今后的工作会有帮助。感谢阅读。

一节聊到正则表达式的简单应用,不足之处欢迎留言交流。

Javascript正则表达式示例之基本概念

今天,我们来看一下,如何使用正则表达式,匹配HTML标签及相关信息。

为什么要加上相关信息呢?

因为,如果您想写一个HTML语法树解析库的时候,可能会用到。


下面内容用到的语法

|:表示或者,要么前面,要么后面

(?<=我前面出现的内容)要匹配的内容:只匹配前面出现的字符之后的内容。

可视图

要匹配的内容(?=我前面出现的内容):只匹配后面出现的字符之前的内容。

可视图

分组捕获:一对完整的小括号(),表示一个组。

\数字:你要使用那一个分组捕获到的内容。

.*?:在正则表达式中,. 表示匹配任意字符,* 表示匹配 0 到任意次的前一个字符,? 表示非贪婪匹配,即尽可能匹配最少的字符。因此,.*? 表示匹配任意字符零次或多次,但尽可能匹配最少的字符。这个表达式通常用于匹配一个字符串中的所有内容,但是避免贪婪匹配导致的匹配错误。

^: 表示匹配开始

[要匹配的字符]:只匹配括号中的字符。

比如[0-9]、[a-z]、[A-Z]、[0-9a-zA-Z]、[0-9abc]等等。

[^要匹配的字符]:[]中加^表示匹配不是“要匹配的字符”。


1、匹配所有HTML标签,并清空。实现innerText类似的功能。

<body><div id="left">left</div><div id="right">right</div></body>
const text = document.body.innerText;
text = text.replace(/\n/g, '');
console.log(text);
//输出: leftright

假设没有innerText的功能呢?实现这个功能,使用正则表达式无疑是最方便的。

var text = document.body.innerHTML.replace(/<[^>]+>/g,'');
text = text.replace(/\n/g, '');
console.log(text);
//输出: leftright

匹配结果

可视图


是的,这个正则表达式的意思是,查找<>并且包含他们之间不为>的一段字符串。

到这里,您以为就结束了吗?您在网上搜索匹配HTML标签,可能也会得到这么一个结果(例如:<[^>]+>、<.*?>、等等),但实际上这只是开始,我们本着只要是程序就可能有bug的原则,所以我们来看下面一个例子。

const strHtml = '<span data-code=">">>是大于符号。</span>';
const strRes = strHtml.replace(/<[^>]+>/g, '');
console.log(strRes);
// ">>是大于符号。

[可怜]bug出现了,怎么办?别着急,请看下一个知识点。


2、匹配HTML标签属性,是写一个HTML语法树要经历的事情。


2.1、首先,我们先解决第一点最后的bug。

const strHtml = '<span data-code=">">>是大于符号。</span>';
// 一个小改动即可。
const strRes = strHtml.replace(/<("[^"]*"|[^>])+>/g, '');
console.log(strRes);
// >是大于符号。

可视图

完美[打脸] ,还没结束……

const strHtml = "<span data-code='>'>>是大于符号。</span>";
const strRes = strHtml.replace(/<("[^"]*"|[^>])+>/g, '');
console.log(strRes);
// '>>是大于符号。

甲:这不是我写的HTML不标准,是你的解析库兼容性不好,浏览器都可以识别,你为什么不可以?

已:……。

const strHtml = `<i code="<"><小于符号。</i><i code='>'>>大于符号。</i>`;
// 继续改造
const strRes = strHtml.replace(/<((["'])+.*?\2|[^>])+>/g, '');
console.log(strRes);
// <小于符号。>大于符号。

匹配结果

可视图

是的,利用正则表达式分组捕获的语法,实现了上面的需求。


2.2 现在,我们来看看,如何找到某个标签的所有属性。

const strHtml = `
<input type='text' disabled value="" class="txt txt-md" v-on:click="save('button')" />
`;

上面的例子中,有多种情况,我们首先来整理出来。

属性1:type='text'

/[\w]+=(["'])+.*?/

属性2:disabled

/[\w]+/

属性3:value=""

/[\w]+=(["'])+.*?/

属性4:class="txt txt-md"

/[\w]+=(["'])+.*?/

属性5:v-on:click="save('button')"

/[\w:]+=(["'])+.*?/

其他情况:欢迎讨论。

把所有情况连起来之后。

const strHtml = `<input type='text' disabled value="" class="txt txt-md" v-on:click="save('button')" />`;
const tagAttrs = strHtml.match(/(?<=\s)[\w:-]+(=(["']).*?\2)*/g) || [];
console.log(tagAttrs);
//  ["type='text'", 'disabled', 'value=""', 'class="txt txt-md"', `v-on:click="save('button')"`]

匹配结果

可视图

人人为我,我为人人,欢迎您的浏览,我们一起加油吧。

JavaScript 画一棵树?

产品说要让前端用 JavaScript 画一棵树出来,但是这难道不能直接让 UI 给一张图片吗?

后来一问才知道,产品要的是一颗 随机树,也就是树的茂盛程度、长度、枝干粗细都是随机的,那这确实没办法叫 UI 给图,毕竟 UI 不可能给我 10000 张树的图片吧?

所以第一时间想到的就是 Canvas,用它来画这棵随机树(文末有完整代码)

Canvas 画一颗随机树

接下来使用 Canvas 去画这棵随机树

基础页面

我们需要在页面上写一个 canvas 标签,并设置好宽高,同时需要获取它的 Dom 节点、绘制上下文,以便后续的绘制

坐标调整

默认的 Canvas 坐标系是这样的

但是我们现在需要从中间去向上去画一棵树,所以坐标得调整成这样:

  • X 轴从最上面移动到最下面
  • Y 轴的方向由往下调整成往上,并且从最左边移动到画布中间

这些操作可以使用 Canvas 的方法

  • ctx.translate: 坐标系移动
  • ctx.scale: 坐标系缩放

绘制一棵树的要素

绘制一棵树的要素是什么呢?其实就是树枝果实,但是其实树枝才是第一要素,那么树枝又有哪些要素呢?无非就这几个点

  • 起始点
  • 树枝长度、树枝粗细
  • 生长角度
  • 终点

开始绘制

所以我们可以写一个 drawBranch 来进行绘制,并且初始调用肯定是绘制树干,树干的参数如下:

  • 起始点:(0, 0)
  • 树枝长度、树枝粗细:这些可以自己自定义
  • 生长角度:90度
  • 终点:需要算

这个终点应该怎么算呢?其实很简单,根据树枝长度、生长角度就可以算出来了,这是初高中的知识

于是我们可以使用 Canvas 的绘制方法,去绘制线段,其实树枝就是一个一个的线段

到现在我绘制出了一个树干 出来

但是我们是想让这棵树开枝散叶,所以需要继续递归继续去绘制更多的树枝出来~

递归绘制

其实往哪开枝散叶呢?无非就是往左或者往右

所以需要递归画左边和右边的树枝,并且子树枝肯定要比父树枝更短、比父树枝更细,比如我们可以定义一个比例

  • 子树枝是父树枝长度的 0.8
  • 子树枝是父树枝粗细的 0.75

而子树枝的生长角度,其实可以随机,我们可以在 0° - 30° 之间随机选一个角度,于是增加了递归调用的代码

但是这个时候会发现,报错了,爆栈了,因为我们只递归开始,但却没有在某个时刻递归停止

我们可以自己定义一个停止规则(规则可以自己定义,这会决定你这棵树的茂盛程度):

  • 粗细小于 2 时马上停止
  • (粗细小于 10 时 + 随机数)决定是否停止

现在可以看到我们已经大致绘制出一棵树了

不过还少了树的果实

绘制果实

绘制果实很简单,只需要在绘制树枝结束的时候,去把果实绘制出来就行,其实果实就是一个个的白色实心圆

至此这棵树完整绘制完毕

绘制部分的代码如下

完整代码