關(guān)于前端要知道的?AST知識
認(rèn)識 AST
定義:在計算機(jī)科學(xué)中,抽象語法樹是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個節(jié)點都表示源代碼中的一種結(jié)構(gòu)。之所以說語法是“抽象”的,是因為這里的語法并不會表示出真實語法中出現(xiàn)的每個細(xì)節(jié)。
從定義中我們只需要知道一件事就行,那就是 AST 是一種樹形結(jié)構(gòu),并且是某種代碼的一種抽象表示。
在線可視化網(wǎng)站:https://astexplorer.net/ ,利用這個網(wǎng)站我們可以很清晰的看到各種語言的 AST 結(jié)構(gòu)。
estree[1]
estree 就是 es 語法對應(yīng)的標(biāo)準(zhǔn) AST,作為一個前端也比較方便理解。我們以官方文檔為例
https://github.com/estree/estree/blob/master/es5.md
下面看一個代碼
console.log('1')AST 為
{
"type": "Program",
"start": 0, // 起始位置
"end": 16, // 結(jié)束位置,字符長度
"body": [
{
"type": "ExpressionStatement", // 表達(dá)式語句
"start": 0,
"end": 16,
"expression": {
"type": "CallExpression", // 函數(shù)方法調(diào)用式
"start": 0,
"end": 16,
"callee": {
"type": "MemberExpression", // 成員表達(dá)式 console.log
"start": 0,
"end": 11,
"object": {
"type": "Identifier", // 標(biāo)識符,可以是表達(dá)式或者結(jié)構(gòu)模式
"start": 0,
"end": 7,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 8,
"end": 11,
"name": "log"
},
"computed": false, // 成員表達(dá)式的計算結(jié)果,如果為 true 則是 console[log], false 則為 console.log
"optional": false
},
"arguments": [ // 參數(shù)
{
"type": "Literal", // 文字標(biāo)記,可以是表達(dá)式
"start": 12,
"end": 15,
"value": "1",
"raw": "'1'"
}
],
"optional": false
}
}
],
"sourceType": "module"
}看兩個稍微復(fù)雜的代碼
const b = { a: 1 };
const { a } = b;function add(a, b) {
return a + b;
}這里建議讀者自己將上述代碼復(fù)制進(jìn)上面提到的網(wǎng)站中,自行理解 estree 的各種節(jié)點類型。當(dāng)然了,我們也不可能看一篇文章就記住那么多類型,只要心里有個大致的概念即可。
認(rèn)識 acorn[2]
由 JavaScript 編寫的 JavaScript 解析器,類似的解析器還有很多,比如 Esprima[3] 和 Shift[4] ,關(guān)于他們的性能,Esprima 的官網(wǎng)給了個測試地址[5],但是由于 acron 代碼比較精簡,且 webpack 和 eslint 都依賴 acorn,因此我們這次從 acorn 下手,了解如何使用 AST。

基本操作
acorn 的操作很簡單
import * as acorn from 'acorn'; const code = 'xxx'; const ast = acorn.parse(code, options)
這樣我們就能拿到代碼的 ast 了,options 的定義如下
interface Options {
ecmaVersion: 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 'latest'
sourceType?: 'script' | 'module'
onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
allowReserved?: boolean | 'never'
allowReturnOutsideFunction?: boolean
allowImportExportEverywhere?: boolean
allowAwaitOutsideFunction?: boolean
allowSuperOutsideMethod?: boolean
allowHashBang?: boolean
locations?: boolean
onToken?: ((token: Token) => any) | Token[]
onComment?: ((
isBlock: boolean, text: string, start: number, end: number, startLoc?: Position,
endLoc?: Position
) => void) | Comment[]
ranges?: boolean
program?: Node
sourceFile?: string
directSourceFile?: string
preserveParens?: boolean
}- ecmaVersion ECMA 版本,默認(rèn)時 es7
- locations 默認(rèn)為 false,設(shè)置為 true 時節(jié)點會攜帶一個 loc 對象來表示當(dāng)前開始與結(jié)束的行數(shù)。
- onComment 回調(diào)函數(shù),每當(dāng)代碼執(zhí)行到注釋的時候都會觸發(fā),可以獲取當(dāng)前的注釋內(nèi)容
獲得 ast 之后我們想還原之前的函數(shù)怎么辦,這里可以使用 astring[6]
import * as astring from 'astring'; const code = astring.generate(ast);
實現(xiàn)普通函數(shù)轉(zhuǎn)換為箭頭函數(shù)
接下來我們就可以利用 AST 來實現(xiàn)一些字符串匹配不太容易實現(xiàn)的操作,比如將普通函數(shù)轉(zhuǎn)化為箭頭函數(shù)。
我們先來看兩個函數(shù)的AST有什么區(qū)別
function add(a, b) {
return a + b;
}const add = (a, b) => {
return a + b;
}{
"type": "Program",
"start": 0,
"end": 41,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 40,
"id": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 19,
"end": 40,
"body": [
{
"type": "ReturnStatement",
"start": 25,
"end": 38,
"argument": {
"type": "BinaryExpression",
"start": 32,
"end": 37,
"left": {
"type": "Identifier",
"start": 32,
"end": 33,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 36,
"end": 37,
"name": "b"
}
}
}
]
}
}
],
"sourceType": "module"
}{
"type": "Program",
"start": 0,
"end": 43,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 43,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 43,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "add"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 12,
"end": 43,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 22,
"end": 43,
"body": [
{
"type": "ReturnStatement",
"start": 28,
"end": 41,
"argument": {
"type": "BinaryExpression",
"start": 35,
"end": 40,
"left": {
"type": "Identifier",
"start": 35,
"end": 36,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 39,
"end": 40,
"name": "b"
}
}
}
]
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}找到區(qū)別之后我們就可以有大致的思路
- 找到
FunctionDeclaration - 將其替換為
VariableDeclarationVariableDeclarator節(jié)點 - 在
VariableDeclarator節(jié)點的init屬性下新建ArrowFunctionExpression節(jié)點 - 并將
FunctionDeclaration節(jié)點的相關(guān)屬性替換到ArrowFunctionExpression上即可
但是由于 acorn 處理的 ast 只是單純的對象,并不具備類似 dom 節(jié)點之類的對節(jié)點的操作能力,如果需要操作節(jié)點,需要寫很多工具函數(shù), 所以我這里就簡單寫一下。
import * as acorn from "acorn";
import * as astring from 'astring';
import { createNode, walkNode } from "./utils.js";
const code = 'function add(a, b) { return a+b; } function dd(a) { return a + 1 }';
console.log('in:', code);
const ast = acorn.parse(code);
walkNode(ast, (node) => {
if(node.type === 'FunctionDeclaration') {
node.type = 'VariableDeclaration';
const variableDeclaratorNode = createNode('VariableDeclarator');
variableDeclaratorNode.id = node.id;
delete node.id;
const arrowFunctionExpressionNode = createNode('ArrowFunctionExpression');
arrowFunctionExpressionNode.params = node.params;
delete node.params;
arrowFunctionExpressionNode.body = node.body;
delete node.body;
variableDeclaratorNode.init = arrowFunctionExpressionNode;
node.declarations = [variableDeclaratorNode];
node.kind = 'const';
}
})
console.log('out:', astring.generate(ast))結(jié)果如下

如果想要代碼更加健壯,可以使用 recast[7],提供了對 ast 的各種操作
// 用螺絲刀解析機(jī)器
const ast = recast.parse(code);
// ast可以處理很巨大的代碼文件
// 但我們現(xiàn)在只需要代碼塊的第一個body,即add函數(shù)
const add = ast.program.body[0]
console.log(add)
// 引入變量聲明,變量符號,函數(shù)聲明三種“模具”
const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders
// 將準(zhǔn)備好的組件置入模具,并組裝回原來的ast對象。
ast.program.body[0] = variableDeclaration("const", [
variableDeclarator(add.id, functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
))
]);
//將AST對象重新轉(zhuǎn)回可以閱讀的代碼
const output = recast.print(ast).code;
console.log(output)這里只是示例代碼,展示 recast 的一些操作,最好的情況還是能遍歷節(jié)點自動替換。
這樣我們就完成了將普通函數(shù)轉(zhuǎn)換成箭頭函數(shù)的操作,但 ast 的作用不止于此,作為一個前端在工作中可能涉及 ast 的地方,就是自定義 eslint 、 stylelint 等插件。
到此這篇關(guān)于關(guān)于前端要知道的 AST知識的文章就介紹到這了,更多相關(guān)前端AST知識內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
簡介JavaScript中toTimeString()方法的使用
這篇文章主要介紹了簡介JavaScript中toTimeString()方法的使用,是JS入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-06-06
javascript基礎(chǔ)第一章 JavaScript與用戶端
javascript基礎(chǔ)第一章 JavaScript與用戶端2010-07-07
詳解JavaScript中g(shù)etFullYear()方法的使用
這篇文章主要介紹了詳解JavaScript中g(shù)etFullYear()方法的使用,是JS入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-06-06
getElementById在任意一款瀏覽器中都可以用嗎的疑問回復(fù)
getElementById在任意一款瀏覽器中都可以用嗎的疑問回復(fù)...2007-05-05

