编辑推荐: |
本文主要介绍了AST 抽象语法树入门知识。 希望能为大家提供一些参考或帮助。
文章来自于稀土掘金,由火龙果Linda编辑推荐。 |
|
AST 是什么
抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
AST 有什么用
AST 运用广泛,比如:
编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
elint、pretiier 对代码错误或风格的检查;
webpack 通过 babel 转译 javascript 语法;
并且如果你想了解 js 编译执行的原理,那么你就得了解 AST。
AST 如何生成
js 执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析(
Parser )生成 AST,最后生成机器码执行。
整个解析过程主要分为以下两个步骤:
分词:将整个代码字符串分割成最小语法单元数组
语法分析:在分词基础上建立分析语法单元之间的关系
JS Parser 是 js 语法解析器,它可以将 js 源码转成 AST,常见的 Parser 有
esprima、traceur、acorn、shift 等。
词法分析
词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的
JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元:
例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。
词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个
Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。
最终,整个代码将被分割进一个tokens列表(或者说一维数组)。
语法分析
语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
说了这么多我们来看下 javaScript 代码片段转成 AST 之后是什么样的我们拿一行简单的代码来展示
例子 1
如图从这个 AST 语法树我们就能够很清楚的看出一个代码他的具体含义,并且使用的是什么语法,方法等。
用人话翻译这个图就是:用类型 const 声明变量 fn 指向一个箭头函数表达式,它的参数是 a 函数体也是
a。
例子 2
const fn = a => {
let i = 1;
return a + i;
};
|
我们来看 body 这块:
例子 3
函数调用
function test(){
let a = 1;
console.log(a)
}
|
主要看 MemberExpression
以上截图均是使用 Acorn 解析。使用 Acorn 的原因是据我了解在 parser 解析中,Acorn
是公认的最快的。并且我们使用的 Webpack 打包工具中 babel 用的也是 Acorn。
上述截图的属性是 AST 的一部分,这个结构包含了很多属性。
VariableDeclaration 变量声明
VariableDeclarator 变量声明的描述
Expression 表达式节点
…
更多属性展示:
可以去 AST explorer 可以在线看到不同的 parser 解析 js 代码后得到的 AST。
github 上看所有的 ESTree ESTree
关于属性介绍的文档 抽象语法树AST介绍
实战 AST 的运用
题目
通过上面介绍的 console.log AST,下面我们就来完成一个在调用 console.log(xx)
时候给前面加一个函数名,这样用户在打印时候能改方便看到是哪个函数调用的。
举例
function getData() {
console.log("data")
}
function getData() {
console.log("getData", "data");
}
|
介绍
首先介绍下我们需要使用的工具 Babel
@babel/parser : 将 js 代码 ------->>> AST 抽象语法树;
@babel/traverse 对 AST 节点进行递归遍历;
@babel/types 对具体的 AST 节点进行进行修改;
@babel/generator : AST 抽象语法树 ------->>> 新的
js 代码;
为什么使用 babel ? 主要是比较好用(只对这个比较熟悉��)。
进入 @babel/parser 官网开头就介绍了它是使用的 Acorn 来解析 js 代码成 AST
语法树(说明确实 Acorn 比较好)。
开始码起来
新建文件打开控制台安装需要的包
cnpm i @babel/parser @babel/traverse @babel/types @babel/generator -D
|
复制代码cnpm i @babel/parser @babel/traverse @babel/types
@babel/generator -D
创建 js 文件, 编写大致布局如下 使用 AST
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
function compile(code) {
const ast = parser.parse(code);
traverse.default(ast, {});
return generator.default(ast, {}, code);
}
const code = `
function getData() {
console.log("data")
}
`;
const newCode = compile(code)
|
使用 node 跑出结果,因为什么都没处理,输出的是原代码,
完善 compile 方法
function compile(code) {
const ast = parser.parse(code);
const visitor = {
CallExpression(path) {
const { callee } = path.node;
const isConsoleLog =
types.isMemberExpression(callee) &&
callee.object.name === "console" &&
callee.property.name === "log";
if (isConsoleLog) {
const funcPath = path.findParent(p => {
return p.isFunctionDeclaration();
});
const funcName = funcPath.node.id.name;
path.node.arguments.unshift(types.stringLiteral(funcName));
}
}
};
traverse.default(ast, visitor);
return generator.default(ast, {}, code);
}
|
纯代码看起来比较难理解下面是我将上面的 path.node 写入到文件中给大家看下数据格式。
复制代码{
{
"type": "CallExpression",
"start": 24,
"end": 43,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 21 }
},
"callee": {
"type": "MemberExpression",
"start": 24,
"end": 35,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 13 }
},
"object": {
"type": "Identifier",
"start": 24,
"end": 31,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 9 },
"identifierName": "console"
},
"name": "console"
},
"property": {
"type": "Identifier",
"start": 32,
"end": 35,
"loc": {
"start": { "line": 3, "column": 10 },
"end": { "line": 3, "column": 13 },
"identifierName": "log"
},
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "StringLiteral",
"start": 36,
"end": 42,
"loc": {
"start": { "line": 3, "column": 14 },
"end": { "line": 3, "column": 20 }
},
"extra": { "rawValue": "data", "raw": "'data'" },
"value": "data"
}
]
}
|
我们将不必要的位置信息(start, end, loc)属性删除,对照数据来看代码将会一目了然
再跑该文件
很好,调用 console.log 方法参数前面增加了函数名,完成!!
为了大家能够方便运行,下面是完整代码
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");
function compile(code) {
const ast = parser.parse(code);
const visitor = {
CallExpression(path) {
const { callee } = path.node;
const isConsoleLog =
types.isMemberExpression(callee) &&
callee.object.name === "console" &&
callee.property.name === "log";
if (isConsoleLog) {
const funcPath = path.findParent(p => {
return p.isFunctionDeclaration();
});
const funcName = funcPath.node.id.name;
fs.writeFileSync("./funcPath.json", JSON.stringify(funcPath.node), err => {
if (err) throw err;
console.log("写入成功");
});
path.node.arguments.unshift(types.stringLiteral(funcName));
}
}
};
traverse.default(ast, visitor);
return generator.default(ast, {}, code);
}
const code = `
function getData() {
console.log('data')
}
`;
console.log(compile(code).code);
|
看到这里,如果你觉得都没什么问题,相信你对 AST 已经有了很清楚的认识了,并且对 babel 编译代码也有了一定的理解,以后写
webpack 配置也就不会对 babel 那么陌生了。
总结
为了兼容低版本浏览器 我们也通常会使用 webpack 打包编译我们的代码将 ES6 语法降低版本,比如箭头函数变成普通函数。将
const、let 声明改成 var 等等,他都是通过 AST 来完成的,只不过实现的过程比较复杂,精致。不过也都是这三板斧:
js 语法解析成 AST;
修改 AST;
AST 转成 js 语法;
最后
有时间,大家在尝试完成之后也同样可以试试箭头函数转普通函数等一些常用的代码转换,这样可以很好的加深印象。
|