沒(méi)有AST, IDE中的錯(cuò)誤提示、自動(dòng)補(bǔ)全、重構(gòu)、語(yǔ)法檢查......都玩不轉(zhuǎn)了
張大胖一上班,領(lǐng)導(dǎo)就扔了一個(gè)任務(wù)給他,把項(xiàng)目中的JavaScript代碼做點(diǎn)“小小”的改變:
1. 把 == 改為全等 ===
2. 把parsetInt不標(biāo)準(zhǔn)的調(diào)用改為標(biāo)準(zhǔn)用法 parseInt(xxx)-> parseInt(xxx,10)
對(duì)不熟悉JS的同學(xué)稍微解釋一下:
JS在比較兩個(gè)變量的時(shí)候,雙等號(hào)將執(zhí)行類型轉(zhuǎn)換; 三等號(hào)將進(jìn)行相同的比較,而不進(jìn)行類型轉(zhuǎn)換 (如果類型不同, 只是總會(huì)返回 false );
parseInt(a,10) 表示以十進(jìn)制的方式來(lái)解析。
對(duì)于這些任務(wù),張大胖腦海中馬上閃現(xiàn)出了解決辦法:字符串替換。
對(duì)***個(gè)任務(wù): 找到'==',替換成'==='就行 。
對(duì)第二個(gè)任務(wù): parseInt(xxx) 改成parseInt(xxx,10), 沒(méi)法直接替換,得寫個(gè)正則表達(dá)式,找到那些只有一個(gè)參數(shù)的parseInt字符串,然后加上一個(gè)新的參數(shù):10 。
張大胖對(duì)自己的正則表達(dá)式能力不太自信,如果考慮得不周全,代碼就可能被改壞了。
有沒(méi)有別的辦法?
01抽象語(yǔ)法樹(shù)
使用正則表達(dá)式,只能把JavaScript源代碼當(dāng)做文本來(lái)處理,能力很弱,無(wú)法觸及到JavaScript的語(yǔ)法層面,正則表達(dá)式?jīng)]法知道這個(gè)地方是變量,那個(gè)地方是函數(shù)名.....
如果能把JavaScript源碼轉(zhuǎn)化成結(jié)構(gòu)化的對(duì)象,就可以精確地知道一段代碼中有哪些變量名,函數(shù)名,參數(shù)...... 這樣就可以寫程序就可以進(jìn)行處理了。
張大胖想起來(lái)自己沒(méi)有考及格的《編譯原理》,里邊講到了抽象語(yǔ)法樹(shù)(AST)不就是所謂結(jié)構(gòu)化的東西嗎?
比如表達(dá)式 result = 6+7*3 , 用抽象語(yǔ)法樹(shù)來(lái)表示就是:
如果把所有的JavaScript代碼都轉(zhuǎn)化成這樣一顆AST的樹(shù),那代碼的一切都盡在掌握, 可以任意修改了。
但是這其中有三個(gè)問(wèn)題:
1. 怎么從文本形式的源代碼形成這么一個(gè)AST ?
讓自己寫程序?qū)崿F(xiàn)那就太難了,得做詞法分析,語(yǔ)法分析等等。
2. 如何遍歷這個(gè)AST,來(lái)修改這顆樹(shù)的枝枝葉葉?
比如我想在AST這棵樹(shù)中添加一個(gè)新的節(jié)點(diǎn),該怎么做?
3. 修改完成以后,怎么再次把AST變成文本的源代碼?
張大胖趕緊打開(kāi)Google 搜索,很快便找到了三個(gè)開(kāi)源的工具,正好完成對(duì)應(yīng)的三個(gè)功能:
esprima : 從JavaScript源代碼形成AST
estraverse:遍歷樹(shù)的節(jié)點(diǎn)并修改
escodegen : 把修改完的AST再次轉(zhuǎn)化為源代碼。
02創(chuàng)建AST
說(shuō)干就干,張大胖準(zhǔn)備了一段代碼來(lái)做實(shí)驗(yàn):
- //源碼
- function fun1(opt) {
- if (opt.status == 1) {
- console.log('1');
- }
- if (opt.status == 2) {
- console.log('2');
- }
- }
- function fun2(age) {
- if (parseInt(age) >= 18) {
- console.log('ok 你已經(jīng)成年');
- }
- }
使用esprima,輕輕松松就把它轉(zhuǎn)化成了抽象語(yǔ)法樹(shù)。
- //JS語(yǔ)法樹(shù)模塊
- const esprima = require('esprima');
- //創(chuàng)建AST
- const AST = esprima.parseScript(jsCode);
(由于轉(zhuǎn)成樹(shù)后結(jié)構(gòu)非常大,這里不再展示了, 感興趣的同學(xué)自己可以到http://esprima.org/demo/parse.html 去玩一把, 很有趣。 )
比如: if (parseInt(age) >= 18) 這一句,就被轉(zhuǎn)化成了這樣:
03遍歷修改AST
有了AST,就可以就是遍歷和修改了,還是使用開(kāi)源的工具。
- //JS語(yǔ)法樹(shù)遍歷各節(jié)點(diǎn)
- const estraverse = require('estraverse');
- //從JS語(yǔ)法樹(shù)生成源代碼
- const escodegen = require('escodegen');
- function walkIn(ast){
- estraverse.traverse(ast, {
- enter: (node) => {
- toEqual(node);//把 == 改為全等 ===
- setParseInt(node); //parseInt(a)-> parseInt(a,10)
- }
- });
- }
這個(gè)函數(shù)負(fù)責(zé)把‘==’改成‘===’
- function toEqual(node) {
- if (node.operator === '==') {
- node.operator = '===';
- }
- }
這個(gè)函數(shù)負(fù)責(zé)把parseInt改成標(biāo)準(zhǔn)調(diào)用:
- 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"
- });
- }
- }
經(jīng)過(guò)這個(gè)函數(shù),原來(lái)的 if (parseInt(age) >= 18) 就變成了下圖這樣,相當(dāng)于增加了一個(gè)節(jié)點(diǎn),對(duì)應(yīng)的代碼就是 :if (parseInt(age,10) >= 18)
***使用escodegen 把修改過(guò)的AST再次變成源代碼,就大功告成了:
- //生成目標(biāo)代碼
- const code = escodegen.generate(ast);
- //寫入文件.....
- //....你懂的
通過(guò)這個(gè)實(shí)驗(yàn),張大胖基本上了解了AST的原理和用法,接下來(lái)可以著手正式的編程了。
04總結(jié)
本文的例子用AST也許不是***解, 主要是為了展示AST的處理技術(shù), AST實(shí)際上就是源代碼的一種結(jié)構(gòu)化表示, 利用它及相關(guān)工具可以方便地優(yōu)化和修改代碼,只要是你能對(duì)這棵“AST樹(shù)”做“修剪”就可以對(duì)源代碼做各種“手腳”:
JavaScript代碼語(yǔ)法、風(fēng)格的檢查
在IDE中的錯(cuò)誤提示、自動(dòng)補(bǔ)全,重構(gòu)
代碼的壓縮和混淆 代碼的轉(zhuǎn)換 ......
有這么強(qiáng)大的功能,AST處理技術(shù)是很多知名工具的基礎(chǔ), 例如babel,webpack,還有jd taro等都把AST用得***。
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)作者微信公眾號(hào)coderising獲取授權(quán)】