日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

详解AST抽象语法树

發布時間:2023/12/20 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 详解AST抽象语法树 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

淺談 AST

先來看一下把一個簡單的函數轉換成AST之后的樣子。

// 簡單函數 function square(n) {return n * n; }// 轉換后的AST {type: "FunctionDeclaration",id: {type: "Identifier",name: "square"},params: [{type: "Identifier",name: "n"}],... }

從純文本轉換成樹形結構的數據,每個條目和樹中的節點一一對應。

純文本轉AST的實現

當下的編譯器都做了純文本轉AST的事情。

一款編譯器的編譯流程是很復雜的,但我們只需要關注詞法分析和語法分析,這兩步是從代碼生成AST的關鍵所在。

第一步:詞法分析,也叫掃描scanner

它讀取我們的代碼,然后把它們按照預定的規則合并成一個個的標識 tokens。同時,它會移除空白符、注釋等。最后,整個代碼將被分割進一個 tokens 列表(或者說一維數組)。

const a = 5;// 轉換成[{value: 'const', type: 'keyword'}, {value: 'a', type: 'identifier'}, ...]

當詞法分析源代碼的時候,它會一個一個字母地讀取代碼,所以很形象地稱之為掃描 - scans。當它遇到空格、操作符,或者特殊符號的時候,它會認為一個話已經完成了。

第二步:語法分析,也稱解析器

它會將詞法分析出來的數組轉換成樹形的形式,同時,驗證語法。語法如果有錯的話,拋出語法錯誤。

[{value: 'const', type: 'keyword'}, {value: 'a', type: 'identifier'}, ...]// 語法分析后的樹形形式{type: "VariableDeclarator",id: {type: "Identifier",name: "a"},...}

當生成樹的時候,解析器會刪除一些沒必要的標識 tokens(比如:不完整的括號),因此 AST 不是 100% 與源碼匹配的。

解析器100%覆蓋所有代碼結構生成樹叫做CST(具體語法樹)。

用例:代碼轉換之babel

babel 是一個 JavaScript 編譯器。宏觀來說,它分3個階段運行代碼:解析(parsing) — 將代碼字符串轉換成 AST抽象語法樹,轉譯(transforming) — 對抽象語法樹進行變換操作,生成(generation) — 根據變換后的抽象語法樹生成新的代碼字符串。

我們給 babel 一段 js 代碼,它修改代碼然后生成新的代碼返回。它是怎么修改代碼的呢?沒錯,它創建了 AST,遍歷樹,修改 tokens,最后從 AST中生成新的代碼。

詳解 AST

前言

抽象語法樹(AST),是一個非常基礎而重要的知識點,但國內的文檔卻幾乎一片空白。

本文將帶大家從底層了解AST,并且通過發布一個小型前端工具,來帶大家了解AST的強大功能

Javascript就像一臺精妙運作的機器,我們可以用它來完成一切天馬行空的構思。

我們對javascript生態了如指掌,卻常忽視javascript本身。這臺機器,究竟是哪些零部件在支持著它運行?

AST在日常業務中也許很難涉及到,但當你不止于想做一個工程師,而想做工程師的工程師,寫出vue、react之類的大型框架,或類似webpack、vue-cli前端自動化的工具,或者有批量修改源碼的工程需求,那你必須懂得AST。AST的能力十分強大,且能幫你真正吃透javascript的語言精髓。

事實上,在javascript世界中,你可以認為抽象語法樹(AST)是最底層。 再往下,就是關于轉換和編譯的“黑魔法”領域了。

人生第一次拆解Javascript

小時候,當我們拿到一個螺絲刀和一臺機器,人生中最令人懷念的夢幻時刻便開始了:

我們把機器,拆成一個一個小零件,一個個齒輪與螺釘,用巧妙的機械原理銜接在一起…

當我們把它重新照不同的方式組裝起來,這時,機器重新又跑動了起來——世界在你眼中如獲新生。

通過抽象語法樹解析,我們可以像童年時拆解玩具一樣,透視Javascript這臺機器的運轉,并且重新按著你的意愿來組裝。

現在,我們拆解一個簡單的add函數

function add(a, b) {return a + b}

首先,我們拿到的這個語法塊,是一個FunctionDeclaration(函數定義)對象。

用力拆開,它成了三塊:

  • 一個id,就是它的名字,即add

  • 兩個params,就是它的參數,即[a, b]

  • 一塊body,也就是大括號內的一堆東西

add沒辦法繼續拆下去了,它是一個最基礎Identifier(標志)對象,用來作為函數的唯一標志,就像人的姓名一樣。

{name: 'add'type: 'identifier'...}

params繼續拆下去,其實是兩個Identifier組成的數組。之后也沒辦法拆下去了。

[{name: 'a'type: 'identifier'...},{name: 'b'type: 'identifier'...} ]

接下來,我們繼續拆開body

我們發現,body其實是一個BlockStatement(塊狀域)對象,用來表示是{return a + b}

打開Blockstatement,里面藏著一個ReturnStatement(Return域)對象,用來表示return a + b

繼續打開ReturnStatement,里面是一個BinaryExpression(二項式)對象,用來表示a + b

繼續打開BinaryExpression,它成了三部分,left,operator,right

  • operator 即+

  • left 里面裝的,是Identifier對象 a

  • right 里面裝的,是Identifer對象 b

就這樣,我們把一個簡單的add函數拆解完畢,用圖表示就是

看!抽象語法樹(Abstract Syntax Tree),的確是一種標準的樹結構。

那么,上面我們提到的Identifier、Blockstatement、ReturnStatement、BinaryExpression, 這一個個小部件的說明書去哪查?

送給你的AST螺絲刀:recast

輸入命令:

npm i recast -S

你即可獲得一把操縱語法樹的螺絲刀

接下來,你可以在任意js文件下操縱這把螺絲刀,我們新建一個parse.js示意:

parse.js

// 給你一把"螺絲刀"——recastconst recast = require("recast");// 你的"機器"——一段代碼// 我們使用了很奇怪格式的代碼,想測試是否能維持代碼結構const code =`function add(a, b) {return a +// 有什么奇怪的東西混進來了b}`// 用螺絲刀解析機器const ast = recast.parse(code);// ast可以處理很巨大的代碼文件// 但我們現在只需要代碼塊的第一個body,即add函數const add = ast.program.body[0]console.log(add)

輸入node parse.js你可以查看到add函數的結構,與之前所述一致,通過AST對象文檔可查到它的具體屬性:

FunctionDeclaration{type: 'FunctionDeclaration',id: ...params: ...body: ...}

你也可以繼續使用console.log透視它的更內層,如:

console.log(add.params[0])console.log(add.body.body[0].argument.left)

recast.types.builders 制作模具

一個機器,你只會拆開重裝,不算本事。

拆開了,還能改裝,才算上得了臺面。

recast.types.builders里面提供了不少“模具”,讓你可以輕松地拼接成新的機器。

最簡單的例子,我們想把之前的function add(a, b){…}聲明,改成匿名函數式聲明const add = function(a ,b){…}

如何改裝?

第一步,我們創建一個VariableDeclaration變量聲明對象,聲明頭為const, 內容為一個即將創建的VariableDeclarator對象。

第二步,創建一個VariableDeclarator,放置add.id在左邊, 右邊是將創建的FunctionDeclaration對象

第三步,我們創建一個FunctionDeclaration,如前所述的三個組件,id params body中,因為是匿名函數id設為空,params使用add.params,body使用add.body。

這樣,就創建好了const add = function(){}的AST對象。

在之前的parse.js代碼之后,加入以下代碼

// 引入變量聲明,變量符號,函數聲明三種“模具”const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders// 將準備好的組件置入模具,并組裝回原來的ast對象。ast.program.body[0] = variableDeclaration("const", [variableDeclarator(add.id, functionExpression(null, // Anonymize the function expression.add.params,add.body))]);//將AST對象重新轉回可以閱讀的代碼const output = recast.print(ast).code;console.log(output)

可以看到,我們打印出了

const add = function(a, b) {return a +// 有什么奇怪的東西混進來了b};

最后一行

const output = recast.print(ast).code;

其實是recast.parse的逆向過程,具體公式為

recast.print(recast.parse(source)).code === source

打印出來還保留著“原裝”的函數內容,連注釋都沒有變。

我們其實也可以打印出美化格式的代碼段:

const output = recast.prettyPrint(ast, { tabWidth: 2 }).code

輸出為

const add = function(a, b) {return a + b;};

現在,你是不是已經產生了“我可以通過AST樹生成任何js代碼”的幻覺?我鄭重告訴你,這不是幻覺。

實戰進階:命令行修改js文件

除了parse/print/builder以外,Recast的三項主要功能:

  • run: 通過命令行讀取js文件,并轉化成ast以供處理。

  • tnt: 通過assert()和check(),可以驗證ast對象的類型。

  • visit: 遍歷ast樹,獲取有效的AST對象并進行更改。

我們通過一個系列小務來學習全部的recast工具庫:

創建一個用來示例文件,假設是demo.js

demo.js

function add(a, b) {return a + b}function sub(a, b) {return a - b}function commonDivision(a, b) {while (b !== 0) {if (a > b) {a = sub(a, b)} else {b = sub(b, a)}}return a}

recast.run —— 命令行文件讀取

新建一個名為read.js的文件,寫入

read.js

recast.run( function(ast, printSource){printSource(ast)})

命令行輸入

node read demo.js

我們查以看到js文件內容打印在了控制臺上。

我們可以知道,node read可以讀取demo.js文件,并將demo.js內容轉化為ast對象。

同時它還提供了一個printSource函數,隨時可以將ast的內容轉換回源碼,以方便調試。

recast.visit —— AST節點遍歷

read.js

#!/usr/bin/env nodeconst recast = require('recast')recast.run(function(ast, printSource) {recast.visit(ast, {visitExpressionStatement: function({node}) {console.log(node)return false}});});

recast.visit將AST對象內的節點進行逐個遍歷。

注意

  • 你想操作函數聲明,就使用visitFunctionDelaration遍歷,想操作賦值表達式,就使用visitExpressionStatement。 只要在 AST對象文檔中定義的對象,在前面加visit,即可遍歷。

  • 通過node可以取到AST對象

  • 每個遍歷函數后必須加上return false,或者選擇以下寫法,否則報錯:

  • #!/usr/bin/env nodeconst recast = require('recast')recast.run(function(ast, printSource) {recast.visit(ast, {visitExpressionStatement: function(path) {const node = path.nodeprintSource(node)this.traverse(path)}})});

    ?

調試時,如果你想輸出AST對象,可以console.log(node)

如果你想輸出AST對象對應的源碼,可以printSource(node)

命令行輸入node read demo.js進行測試。

#!/usr/bin/env node

在所有使用recast.run()的文件頂部都需要加入這一行,它的意義我們最后再討論。

TNT —— 判斷AST對象類型

TNT,即recast.types.namedTypes,就像它的名字一樣火爆,它用來判斷AST對象是否為指定的類型。

TNT.Node.assert(),就像在機器里埋好的炸藥,當機器不能完好運轉時(類型不匹配),就炸毀機器(報錯退出)

TNT.Node.check(),則可以判斷類型是否一致,并輸出False和True

上述Node可以替換成任意AST對象,例如TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert()

read.js

#!/usr/bin/env nodeconst recast = require("recast");const TNT = recast.types.namedTypesrecast.run(function(ast, printSource) {recast.visit(ast, {visitExpressionStatement: function(path) {const node = path.value// 判斷是否為ExpressionStatement,正確則輸出一行字。if(TNT.ExpressionStatement.check(node)){console.log('這是一個ExpressionStatement')}this.traverse(path);}});});

read.js

#!/usr/bin/env nodeconst recast = require("recast");const TNT = recast.types.namedTypesrecast.run(function(ast, printSource) {recast.visit(ast, {visitExpressionStatement: function(path) {const node = path.node// 判斷是否為ExpressionStatement,正確不輸出,錯誤則全局報錯TNT.ExpressionStatement.assert(node)this.traverse(path);}});});

命令行輸入node read demo.js進行測試。

實戰:用AST修改源碼,導出全部方法

exportific.js

現在,我們希望將demo中的function全部

我們想讓這個文件中的函數改寫成能夠全部導出的形式,例如

?
  • function add (a, b) {

  • ? ?return a + b

  • }

  • 想改變為

    ?
  • exports.add = (a, b) => {

  • ?return a + b

  • }

  • 除了使用fs.read讀取文件、正則匹配替換文本、fs.write寫入文件這種笨拙的方式外,我們可以==用AST優雅地解決問題==。

    首先,我們先用builders憑空實現一個鍵頭函數

    exportific.js

    #!/usr/bin/env nodeconst recast = require("recast");const {identifier:id,expressionStatement,memberExpression,assignmentExpression,arrowFunctionExpression,blockStatement} = recast.types.buildersrecast.run(function(ast, printSource) {// 一個塊級域 {}console.log('\n\nstep1:')printSource(blockStatement([]))// 一個鍵頭函數 ()=>{}console.log('\n\nstep2:')printSource(arrowFunctionExpression([],blockStatement([])))// add賦值為鍵頭函數 add = ()=>{}console.log('\n\nstep3:')printSource(assignmentExpression('=',id('add'),arrowFunctionExpression([],blockStatement([]))))// exports.add賦值為鍵頭函數 exports.add = ()=>{}console.log('\n\nstep4:')printSource(expressionStatement(assignmentExpression('=',memberExpression(id('exports'),id('add')),arrowFunctionExpression([],blockStatement([])))))});

    上面寫了我們一步一步推斷出exports.add = ()=>{}的過程,從而得到具體的AST結構體。

    使用node exportific demo.js運行可查看結果。

    接下來,只需要在獲得的最終的表達式中,把id(‘add’)替換成遍歷得到的函數名,把參數替換成遍歷得到的函數參數,把blockStatement([])替換為遍歷得到的函數塊級作用域,就成功地改寫了所有函數!

    另外,我們需要注意,在commonDivision函數內,引用了sub函數,應改寫成exports.sub

    exportific.js

    #!/usr/bin/env nodeconst recast = require("recast");const {identifier: id,expressionStatement,memberExpression,assignmentExpression,arrowFunctionExpression} = recast.types.buildersrecast.run(function (ast, printSource) {// 用來保存遍歷到的全部函數名let funcIds = []recast.types.visit(ast, {// 遍歷所有的函數定義visitFunctionDeclaration(path) {//獲取遍歷到的函數名、參數、塊級域const node = path.nodeconst funcName = node.idconst params = node.paramsconst body = node.body// 保存函數名funcIds.push(funcName.name)// 這是上一步推導出來的ast結構體const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),arrowFunctionExpression(params, body)))// 將原來函數的ast結構體,替換成推導ast結構體path.replace(rep)// 停止遍歷return false}})recast.types.visit(ast, {// 遍歷所有的函數調用visitCallExpression(path){const node = path.node;// 如果函數調用出現在函數定義中,則修改ast結構if (funcIds.includes(node.callee.name)) {node.callee = memberExpression(id('exports'), node.callee)}// 停止遍歷return false}})// 打印修改后的ast源碼printSource(ast)})

    一步到位,發一個最簡單的exportific前端工具

    上面講了那么多,仍然只體現在理論階段。

    但通過簡單的改寫,就能通過recast制作成一個名為exportific的源碼編輯工具。

    以下代碼添加作了兩個小改動

    • 添加說明書—help,以及添加了—rewrite模式,可以直接覆蓋文件或默認為導出*.export.js文件。

    • 將之前代碼最后的 printSource(ast)替換成 writeASTFile(ast,filename,rewriteMode)

    exportific.js

    #!/usr/bin/env nodeconst recast = require("recast");const {identifier: id,expressionStatement,memberExpression,assignmentExpression,arrowFunctionExpression} = recast.types.buildersconst fs = require('fs')const path = require('path')// 截取參數const options = process.argv.slice(2)//如果沒有參數,或提供了-h 或--help選項,則打印幫助if(options.length===0 || options.includes('-h') || options.includes('--help')){console.log(`采用commonjs規則,將.js文件內所有函數修改為導出形式。選項: -r 或 --rewrite 可直接覆蓋原有文件`)process.exit(0)}// 只要有-r 或--rewrite參數,則rewriteMode為truelet rewriteMode = options.includes('-r') || options.includes('--rewrite')// 獲取文件名const clearFileArg = options.filter((item)=>{return !['-r','--rewrite','-h','--help'].includes(item)})// 只處理一個文件let filename = clearFileArg[0]const writeASTFile = function(ast, filename, rewriteMode){const newCode = recast.print(ast).codeif(!rewriteMode){// 非覆蓋模式下,將新文件寫入*.export.js下filename = filename.split('.').slice(0,-1).concat(['export','js']).join('.')}// 將新代碼寫入文件fs.writeFileSync(path.join(process.cwd(),filename),newCode)}recast.run(function (ast, printSource) {let funcIds = []recast.types.visit(ast, {visitFunctionDeclaration(path) {//獲取遍歷到的函數名、參數、塊級域const node = path.nodeconst funcName = node.idconst params = node.paramsconst body = node.bodyfuncIds.push(funcName.name)const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),arrowFunctionExpression(params, body)))path.replace(rep)return false}})recast.types.visit(ast, {visitCallExpression(path){const node = path.node;if (funcIds.includes(node.callee.name)) {node.callee = memberExpression(id('exports'), node.callee)}return false}})writeASTFile(ast,filename,rewriteMode)})

    現在嘗試一下

    node exportific demo.js

    已經可以在當前目錄下找到源碼變更后的demo.export.js文件了。

    npm發包

    編輯一下package.json文件

    {"name": "exportific","version": "0.0.1","description": "改寫源碼中的函數為可exports.XXX形式","main": "exportific.js","bin": {"exportific": "./exportific.js"},"keywords": [],"author": "wanthering","license": "ISC","dependencies": {"recast": "^0.15.3"}}

    注意bin選項,它的意思是將全局命令exportific指向當前目錄下的exportific.js

    這時,輸入npm link 就在本地生成了一個exportific命令。

    之后,只要哪個js文件想導出來使用,就exportific XXX.js一下。

    這是在本地的玩法,想和大家一起分享這個前端小工具,只需要發布npm包就行了。

    同時,一定要注意exportific.js文件頭有

    #!/usr/bin/env node

    否則在使用時將報錯。

    接下來,正式發布npm包!

    如果你已經有了npm 帳號,請使用npm login登錄

    如果你還沒有npm帳號?https://www.npmjs.com/signup?非常簡單就可以注冊npm

    然后,輸入
    npm publish

    沒有任何繁瑣步驟,絲毫審核都沒有,你就發布了一個實用的前端小工具exportific 。任何人都可以通過

    npm i exportific -g

    全局安裝這一個插件。

    ?

    參考文章:https://segmentfault.com/a/1190000016231512

    ? ? ? ? ? ? ? ? ??https://github.com/CodeLittlePrince/blog/issues/19

    總結

    以上是生活随笔為你收集整理的详解AST抽象语法树的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。