關(guān)于前端要知道的?AST知識(shí)
認(rèn)識(shí) AST
定義:在計(jì)算機(jī)科學(xué)中,抽象語法樹是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。之所以說語法是“抽象”的,是因?yàn)檫@里的語法并不會(huì)表示出真實(shí)語法中出現(xiàn)的每個(gè)細(xì)節(jié)。
從定義中我們只需要知道一件事就行,那就是 AST 是一種樹形結(jié)構(gòu),并且是某種代碼的一種抽象表示。
在線可視化網(wǎng)站:https://astexplorer.net/ ,利用這個(gè)網(wǎng)站我們可以很清晰的看到各種語言的 AST 結(jié)構(gòu)。
estree[1]
estree 就是 es 語法對(duì)應(yīng)的標(biāo)準(zhǔn) AST,作為一個(gè)前端也比較方便理解。我們以官方文檔為例
https://github.com/estree/estree/blob/master/es5.md
下面看一個(gè)代碼
console.log('1')
AST 為
{ "type": "Program", "start": 0, // 起始位置 "end": 16, // 結(jié)束位置,字符長(zhǎng)度 "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)識(shí)符,可以是表達(dá)式或者結(jié)構(gòu)模式 "start": 0, "end": 7, "name": "console" }, "property": { "type": "Identifier", "start": 8, "end": 11, "name": "log" }, "computed": false, // 成員表達(dá)式的計(jì)算結(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" }
看兩個(gè)稍微復(fù)雜的代碼
const b = { a: 1 }; const { a } = b;
function add(a, b) { return a + b; }
這里建議讀者自己將上述代碼復(fù)制進(jìn)上面提到的網(wǎng)站中,自行理解 estree 的各種節(jié)點(diǎn)類型。當(dāng)然了,我們也不可能看一篇文章就記住那么多類型,只要心里有個(gè)大致的概念即可。
認(rèn)識(shí) acorn[2]
由 JavaScript 編寫的 JavaScript 解析器,類似的解析器還有很多,比如 Esprima[3] 和 Shift[4] ,關(guān)于他們的性能,Esprima 的官網(wǎng)給了個(gè)測(cè)試地址[5],但是由于 acron 代碼比較精簡(jiǎn),且 webpack 和 eslint 都依賴 acorn,因此我們這次從 acorn 下手,了解如何使用 AST。
基本操作
acorn 的操作很簡(jiǎn)單
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)時(shí) es7
- locations 默認(rèn)為 false,設(shè)置為 true 時(shí)節(jié)點(diǎn)會(huì)攜帶一個(gè) loc 對(duì)象來表示當(dāng)前開始與結(jié)束的行數(shù)。
- onComment 回調(diào)函數(shù),每當(dāng)代碼執(zhí)行到注釋的時(shí)候都會(huì)觸發(fā),可以獲取當(dāng)前的注釋內(nèi)容
獲得 ast 之后我們想還原之前的函數(shù)怎么辦,這里可以使用 astring[6]
import * as astring from 'astring'; const code = astring.generate(ast);
實(shí)現(xiàn)普通函數(shù)轉(zhuǎn)換為箭頭函數(shù)
接下來我們就可以利用 AST 來實(shí)現(xiàn)一些字符串匹配不太容易實(shí)現(xiàn)的操作,比如將普通函數(shù)轉(zhuǎn)化為箭頭函數(shù)。
我們先來看兩個(gè)函數(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
- 將其替換為
VariableDeclaration
VariableDeclarator
節(jié)點(diǎn) - 在
VariableDeclarator
節(jié)點(diǎn)的init
屬性下新建ArrowFunctionExpression
節(jié)點(diǎn) - 并將
FunctionDeclaration
節(jié)點(diǎn)的相關(guān)屬性替換到ArrowFunctionExpression
上即可
但是由于 acorn 處理的 ast 只是單純的對(duì)象,并不具備類似 dom 節(jié)點(diǎn)之類的對(duì)節(jié)點(diǎn)的操作能力,如果需要操作節(jié)點(diǎn),需要寫很多工具函數(shù), 所以我這里就簡(jiǎn)單寫一下。
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],提供了對(duì) ast 的各種操作
// 用螺絲刀解析機(jī)器 const ast = recast.parse(code); // ast可以處理很巨大的代碼文件 // 但我們現(xiàn)在只需要代碼塊的第一個(gè)body,即add函數(shù) const add = ast.program.body[0] console.log(add) // 引入變量聲明,變量符號(hào),函數(shù)聲明三種“模具” const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders // 將準(zhǔn)備好的組件置入模具,并組裝回原來的ast對(duì)象。 ast.program.body[0] = variableDeclaration("const", [ variableDeclarator(add.id, functionExpression( null, // Anonymize the function expression. add.params, add.body )) ]); //將AST對(duì)象重新轉(zhuǎn)回可以閱讀的代碼 const output = recast.print(ast).code; console.log(output)
這里只是示例代碼,展示 recast 的一些操作,最好的情況還是能遍歷節(jié)點(diǎn)自動(dòng)替換。
這樣我們就完成了將普通函數(shù)轉(zhuǎn)換成箭頭函數(shù)的操作,但 ast 的作用不止于此,作為一個(gè)前端在工作中可能涉及 ast 的地方,就是自定義 eslint 、 stylelint 等插件。
到此這篇關(guān)于關(guān)于前端要知道的 AST知識(shí)的文章就介紹到這了,更多相關(guān)前端AST知識(shí)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript中對(duì)Date類型的常用操作小結(jié)
下面小編就為大家?guī)硪黄猨avascript中對(duì)Date類型的常用操作小結(jié)。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-05-05實(shí)例講解JavaScript中instanceof運(yùn)算符的用法
JavaScript中的instanceof運(yùn)算符可以用來判斷對(duì)象類型,而更重要的是instanceof能夠判斷對(duì)象的繼承關(guān)系,這里我們就來以實(shí)例講解JavaScript中instanceof運(yùn)算符的用法2016-06-06通過實(shí)例了解js函數(shù)中參數(shù)的傳遞
這篇文章主要介紹了通過實(shí)例了解js函數(shù)中參數(shù)的傳遞,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06javascript 學(xué)習(xí)筆記(八)javascript對(duì)象
昨天看了些有關(guān)javascript對(duì)象方面的文章,以下是自己的一些學(xué)習(xí)心得及體會(huì),希望同大家共同討論!2011-04-04javascript基礎(chǔ)知識(shí)分享之類與函數(shù)化
在C++中是以class來聲明一個(gè)類的,JavaScript與C++不同,它使用了與函數(shù)一樣的function來聲明,這就讓許多學(xué)Jscript的朋友把類與函數(shù)混在一起了,在Jscript中函數(shù)與類確實(shí)有些混,但使用久了自然會(huì)理解,這篇文章是針對(duì)想進(jìn)攻面向?qū)ο缶幊痰呐笥讯鴮?就不打算討論得太深了2016-02-02JavaScript入門教程(2) JS基礎(chǔ)知識(shí)
JavaScript 可以出現(xiàn)在 HTML 的任意地方。使用標(biāo)記<script>…</script>,你可以在 HTML 文檔的任意地方插入 JavaScript,甚至在<HTML>之前插入也不成問題。2009-01-01分享我學(xué)習(xí)js的過程 作者aircy javascript學(xué)習(xí)教程
分享我學(xué)習(xí)js的過程 作者aircy javascript學(xué)習(xí)教程...2007-02-02