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