摘要:利用抽象語法樹可以對(duì)你的源代碼進(jìn)行修改優(yōu)化,甚至可以打造自己的編譯工具。
這是一棵樹嘛
抽象語法樹是js代碼另一種結(jié)構(gòu)映射,可以將js拆解成AST,也可以把AST轉(zhuǎn)成源代碼。這中間的過程就是我們的用武之地。 利用 抽象語法樹(AST) 可以對(duì)你的源代碼進(jìn)行修改、優(yōu)化,甚至可以打造自己的編譯工具。其實(shí)有點(diǎn)類似babel的功能。
AST高深的狠嚇人?AST很簡(jiǎn)單,并沒有你想象的那樣高深。很多地方都把這個(gè)技術(shù)給夸大了,什么編譯原理,抽象語法樹 光看這名字就覺得嚇人。當(dāng)然一項(xiàng)技術(shù)總歸要起個(gè)名字,就像給自己的孩子取名字,肯定要起一個(gè)高大上,深有寓意的名字。所以,名字只是一個(gè)代號(hào)。從名字來看就會(huì)讓很多人望而卻步。但是ast超級(jí)簡(jiǎn)單,但是功能超級(jí)強(qiáng)大。
我們能用這個(gè)技術(shù)做很多有意思的東西,只要你能想到的。
本文術(shù)道結(jié)合,讓你感受到ast的有趣和簡(jiǎn)單,從此愛上ast,還能根據(jù)自己的需要打造自己的編譯器。
什么是AST?ast全稱是abstract syntax tree,翻譯過來叫-抽象語法樹。其實(shí)這含兩個(gè)意思,一個(gè)是“抽象”,一個(gè)是“樹”。抽象表示把js代碼進(jìn)行了結(jié)構(gòu)化的轉(zhuǎn)化,轉(zhuǎn)化為一種數(shù)據(jù)結(jié)構(gòu)。這種數(shù)據(jù)結(jié)構(gòu)其實(shí)就是一個(gè)大的json對(duì)象,json我們都熟悉,他就像一顆枝繁葉茂的樹。
有樹根,有樹干,有樹枝,有樹葉.無論多小多大,都是一棵完整的樹。
如何生成AST?你可以大致的想一下如果親自實(shí)現(xiàn)把js代碼轉(zhuǎn)換成結(jié)構(gòu)化的數(shù)據(jù)我們應(yīng)該怎么做?
有點(diǎn)像小時(shí)候拆解自己的玩具,每個(gè)零件之間都有著從屬關(guān)系。
對(duì)于如何生成ast,我們可能會(huì)想到分析js代碼的規(guī)則使用字符串處理、正則匹配等方法,如果對(duì)簡(jiǎn)單的代碼處理我們是可以實(shí)現(xiàn)的。但是如果能夠?qū)﹄S意的一段代碼進(jìn)行處理那就需要考慮非常多的情況。具體如何實(shí)現(xiàn)咱們不必過于糾結(jié),這也不是重點(diǎn)。
但最終的實(shí)現(xiàn)里我們能想到方法基本都會(huì)被用到。我們可以簡(jiǎn)化理解,也就是對(duì)js代碼經(jīng)過了一系列的加工處理,變成了一堆零件或者食材(像老媽給我們做的香噴噴的飯菜,但前提是先準(zhǔn)備好菜)。
這個(gè)拆解的過程可能較為復(fù)雜,所以我們需要用現(xiàn)成方法,直接拿過來用就可以了。
所以我們需要用到esprima、UglifyJS等庫,做菜的食材有很多種,所以會(huì)存在很多這樣的三方庫,而我們會(huì)使用其中一種就可以了。
先使用esprima 種菜,體會(huì)一下
種子:
//源代碼 function fun(a,b){ }
成熟:
{ "type": "FunctionDeclaration",//函數(shù)聲明 "id": { "type": "Identifier",//標(biāo)識(shí)符 "name": "fun" //函數(shù)名稱 }, "params": [//函數(shù)參數(shù) { "type": "Identifier",//參數(shù)標(biāo)識(shí)符 "name": "a"http://參數(shù)名稱 }, { "type": "Identifier", "name": "b" } ], "body": {//函數(shù)體 "type": "BlockStatement",//語句塊兒 "body": []//具體內(nèi)容為空,因?yàn)槭强辗椒? } }有了AST能做什么?
到這一步你已經(jīng)可以把js代碼轉(zhuǎn)換成一棵結(jié)構(gòu)化的樹了,那下一步要做什么呢? 比如在沒有樹的情況下,你要對(duì)代碼里的某個(gè)代碼進(jìn)行替換。要把所有 console.log給注釋掉或者刪除,你可能會(huì)使用IDE的查找替換或者用node寫一個(gè)方法,讀取文件然后查找替換。
這種方式不夠安全也不夠科學(xué),稍有不慎就會(huì)把代碼給搞壞了。
但這個(gè)時(shí)候你有了結(jié)構(gòu)化代碼樹,是不是只要對(duì)這棵樹進(jìn)行修修剪剪然后把這棵樹轉(zhuǎn)換成為js代碼就可以了呢?
答案:肯定是可以的。因?yàn)闃湟呀?jīng)發(fā)生了變化,修改了樹就相當(dāng)于修改了源碼。
怎樣操作這棵樹呢?我想你應(yīng)該已經(jīng)知道了,就是對(duì)這json對(duì)象進(jìn)行操作,方法就多了去了,前提是你得有一點(diǎn)點(diǎn)js基礎(chǔ)。
又一個(gè)問題,怎樣把樹再轉(zhuǎn)成代碼?腦洞打開,用遞歸加字符串拼接,這個(gè)方法應(yīng)該是可以的。
但是這棵樹不是你生成的,結(jié)構(gòu)特點(diǎn)你并不清楚,成千上萬個(gè)節(jié)點(diǎn)呢?怎么拼接?真要干,那可能得搞得流鼻血。
這就像是食材準(zhǔn)備好了,轉(zhuǎn)換成源碼的過程就是炒菜的過程。具體的轉(zhuǎn)源碼的原理不多說,也不必糾結(jié)。使用現(xiàn)成的方法就可以,所以要用到estraverse,escodegen這兩個(gè)庫。
estraverse 可以遍歷樹的所有節(jié)點(diǎn),省去你對(duì)樹的遞歸遍歷
escodegen 可以把樹再加工轉(zhuǎn)成源代碼
過程總結(jié)到這里始終都沒有提到任何代碼,只是理論了一番,但是相信你已經(jīng)理解了ast以及ast的作用。然后在述說過程中引出了3個(gè)庫,有了這三個(gè)庫就可以對(duì)你的js代碼進(jìn)行多樣化處理,只要你能想到的。
看圖理解整個(gè)處理過程:
這個(gè)過程簡(jiǎn)單,清晰,所以說ast簡(jiǎn)單、有趣、好玩。因?yàn)榇丝檀a可以被你任意的蹂躪了。
實(shí)例應(yīng)用說的再清楚都不夠直觀,畢竟都是腦補(bǔ),不如看代碼來的爽快。
這里就拿日常編碼中的一些小問題舉例,來演示一下AST的使用。
把 == 改為全等 ===
把parsetInt不標(biāo)準(zhǔn)的調(diào)用改為標(biāo)準(zhǔn)用法 parseInt(a)-> parseInt(a,10)
這里我使用esprima的官方工具生成了ast,工具地址http://esprima.org/demo/parse...
看下要處理的源碼:
//源碼 function fun1() { console.log("fun1"); } function fun2(opt) { if (opt.status == 1) { console.log("1"); } if (opt.status == 2) { console.log("2"); } } function fun3(age) { if (parseInt(age) >= 18) { console.log("ok 你已經(jīng)成年"); } }
轉(zhuǎn)成ast,由于轉(zhuǎn)成樹后結(jié)構(gòu)非常大,所以這里我只貼了一部分,你也可以到工具頁面自己生成下。
{ "type": "Program", "body": [ { "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "fun1" }, "params": [], "body": { "type": "BlockStatement", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "MemberExpression", "computed": false, "object": { "type": "Identifier", "name": "console" }, "property": { "type": "Identifier", "name": "log" } }, "arguments": [ { "type": "Literal", "value": "fun1", "raw": ""fun1"" } ] } } ] }, "generator": false, "expression": false, "async": false } ] }
ast看上去結(jié)構(gòu)復(fù)雜,盯著仔細(xì)看后基本都能看懂。所有的代碼都在特定的節(jié)點(diǎn)里面。具體的這里就不介紹了,可以到上面的工具地址去觀察不同的ast結(jié)構(gòu)。總之這就是一個(gè)對(duì)象,只要你能對(duì)這個(gè)對(duì)象進(jìn)行修改、添加、刪除即可。
開始實(shí)現(xiàn)以上功能
init
//引入工具包 const esprima = require("esprima");//JS語法樹模塊 const estraverse = require("estraverse");//JS語法樹遍歷各節(jié)點(diǎn) const escodegen = require("escodegen");//JS語法樹反編譯模塊 //獲?取代碼ast const AST = esprima.parseScript(jsCode); /** * * @param {遍歷語法樹} ast */ function walkIn(ast){ estraverse.traverse(ast, { enter: (node) => { toEqual(node);//把 == 改為全等 === setParseint(node); //parseInt(a)-> parseInt(a,10) } }); }
2.把 == 改為全等 ===
/** * 設(shè)置全等 */ function toEqual(node) { if (node.operator === "==") { node.operator = "==="; } }
把parseInt改成標(biāo)準(zhǔn)調(diào)用
/** * 把parseint改為標(biāo)準(zhǔn)方法 * @param {節(jié)點(diǎn)} node */ function setParseint(node) { //判斷節(jié)點(diǎn)類型 方法名稱,方法的參數(shù)的數(shù)量,數(shù)量為1就增加第二個(gè)參數(shù) if (node.type === "CallExpression" && node.callee.name === "parseInt" && node.arguments.length===1){ node.arguments.push({//增加參數(shù),其實(shí)就是數(shù)組操作 "type": "Literal", "value": 10, "raw": "10" }); } } //生成目標(biāo)代碼 const code = escodegen.generate(ast); //寫入文件..... //....你懂的
代碼不多,需求簡(jiǎn)單,但已足夠能說明整個(gè)處理過程以及ast的強(qiáng)大。 ast的節(jié)點(diǎn)很多,有些凌亂,送你一首歌【汪峰的無所謂】,操作的時(shí)候只要關(guān)心你自己的需求就可以,不需要對(duì)所有的節(jié)點(diǎn)都搞明白。按需處理就可以。
AST技術(shù)的應(yīng)用雖然平時(shí)用不到ast,但又時(shí)刻都在使用ast技術(shù)。家喻戶曉、無人不知的babel,webpack,還有jd taro等都把a(bǔ)st用的淋漓盡致,脫離了ast他們就跪了。
AST這么簡(jiǎn)單,好沒技術(shù)含量
AST沒有技術(shù)含量嗎?怎么可能呢,如果真這么認(rèn)為怕是會(huì)被笑掉大牙的。如果僅僅停留在使用層面的話,理解到這步已經(jīng)基本可以了,只要是你能對(duì)這棵樹做修剪就可以對(duì)源代碼做手腳。
另外ast怎樣生成的?怎樣把a(bǔ)st轉(zhuǎn)換成源碼的?這就有點(diǎn)高深了。會(huì)使用就像是在山腳下能看到的風(fēng)景有限,理解了背后原理機(jī)制就像是爬上了山頂,別樣的風(fēng)景盡收眼底。不過上不上山看個(gè)人興趣,有興趣的同學(xué)可以去看源碼、做研究,這里就不再多說,因?yàn)槲乙膊恢?。哈哈?/p> 總結(jié)
本文主要介紹了
什么是ast:
ast其實(shí)就把js代碼進(jìn)行抽象為一種json結(jié)構(gòu);
ast的用途:
利用ast可以方便的優(yōu)化和修改代碼,還能打造自己的編譯器;
然后通過具體的示例演示了怎樣操作ast,最終是希望你能對(duì)ast有一個(gè)系統(tǒng)全局的認(rèn)識(shí)和理解并能夠利用ast打造自己的編譯工具。
演示代碼下載,歡迎star
https://github.com/bigerfe/fo...
自家觀點(diǎn),歡迎打臉
原創(chuàng)不易,請(qǐng)多鼓勵(lì)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/101339.html
摘要:為什么要談抽象語法樹如果你查看目前任何主流的項(xiàng)目中的,會(huì)發(fā)現(xiàn)前些年的不計(jì)其數(shù)的插件誕生。什么是抽象語法樹估計(jì)很多同學(xué)會(huì)和圖中的喵一樣,看完這段官方的定義一臉懵逼。它讀取我們的代碼,然后把它們按照預(yù)定的規(guī)則合并成一個(gè)個(gè)的標(biāo)識(shí)。 前言 首先,先說明下該文章是譯文,原文出自《AST for JavaScript developers》。很少花時(shí)間特地翻譯一篇文章,咬文嚼字是件很累的事情,實(shí)在...
摘要:在開始解析之前,先通過詞法分析器運(yùn)行源碼,這會(huì)將源碼打散成語法中全大寫的部分。我們基于每個(gè)規(guī)則的名稱的左側(cè)為其創(chuàng)建一個(gè)方法,再來看右側(cè)內(nèi)容如果是全大寫的單詞,說明它是一個(gè)終止符即一個(gè),詞法分析器會(huì)用到它。 本文轉(zhuǎn)載自:眾成翻譯譯者:文藺鏈接:http://www.zcfy.cc/article/661原文:http://tadeuzagallo.com/blog/writing-a-l...
摘要:表示的是在嚴(yán)格模式下解析并且允許模塊定義即能識(shí)別和語法識(shí)別不了。 前段時(shí)間開始研究ast,然后慢慢的順便把babel都研究了,至于ast稍后的時(shí)間會(huì)寫一篇介紹性博客專門介紹ast,本博客先介紹一下babel的基本知識(shí)點(diǎn)。 背景: 由于現(xiàn)在前端出現(xiàn)了很多非es5的語法,如jsx,.vue,ts等等的格式和寫法,如果要在瀏覽器的設(shè)備上識(shí)別并執(zhí)行,需要額外將這些非傳統(tǒng)格式的語法轉(zhuǎn)成傳統(tǒng)的es...
摘要:中的用于類型整型和資源類型用于浮點(diǎn)類型用于字符串用于數(shù)組用于對(duì)象用于常量表達(dá)式才有多數(shù)文章,在提到變量結(jié)構(gòu)體的時(shí)候,都提到,實(shí)際上這個(gè)論述并不準(zhǔn)確,在為時(shí),這個(gè)結(jié)果是正確的。主要看中的,是兩個(gè),這個(gè)永遠(yuǎn)是個(gè)字節(jié),所以,因此。 PHP5 中的 zval // 1. zval typedef struct _zval_struct { zvalue_value value; ...
摘要:發(fā)布按照官方發(fā)布計(jì)劃,的發(fā)布意味著進(jìn)入階段,徹底退出舞臺(tái),的還有半年結(jié)束。為了應(yīng)對(duì)這個(gè)挑戰(zhàn),美團(tuán)點(diǎn)評(píng)境外度假前端研發(fā)團(tuán)隊(duì)自年月起啟動(dòng)了面向端用戶的赫爾墨斯項(xiàng)目。前端技術(shù)越來越復(fù)雜,有不低的技術(shù)門檻。 推薦 1. 利用 Dawn 工程化工具實(shí)踐 MobX 數(shù)據(jù)流管理方案 https://zhuanlan.zhihu.com/p/... 項(xiàng)目在最初應(yīng)用 MobX 時(shí),對(duì)較為復(fù)雜的多人協(xié)作項(xiàng)...
閱讀 2357·2023-04-25 14:22
閱讀 3773·2021-11-15 18:12
閱讀 1326·2019-08-30 15:44
閱讀 3244·2019-08-29 15:37
閱讀 762·2019-08-29 13:49
閱讀 3490·2019-08-26 12:11
閱讀 917·2019-08-23 18:28
閱讀 1621·2019-08-23 14:55