抽象語法樹在JavaScript中的應用

來源:文萃谷 1.02W

抽象語法樹是什麼?在 JavaScript 中該如何應用?下面YJBYS小編為大家講解!

抽象語法樹在JavaScript中的應用

計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。樹上的每個節點都表示源代碼中的一種結構。之所以説語法是「抽象」的,是因為這裏的語法並不會表示出真實語法中出現的每個細節。1

果然比較抽象,不如先看幾個例子:

  抽象語法樹舉例

foo = 'hello world';

/*

+-------------+

| assign(=) |

+-------------+

X X

X X

+-------+ +-----------------+

| foo | | 'hello world' |

+-------+ +-----------------+

*/

if (foo === true) {

bar = 'hello world';

alert(bar);

}

/*

+------+

| if |

+------+

X X

X X

+--------------+ +-------------+

| equal(===) | | if_body |

+--------------+ +-------------+

X X X X

X X X X

+-------+ +--------+ +-------------+ +------------+

| foo | | true | | assign(=) | | alert() |

+-------+ +--------+ +-------------+ +------------+

X X X

X X X

+-------+ +-----------------+ +-------+

| bar | | 'hello world' | | bar |

+-------+ +-----------------+ +-------+

*/

從上述兩個例子可以看出,抽象語法樹是將源代碼根據其語法結構,省略一些細節(比如:括號沒有生成節點),抽象成樹形表達。

抽象語法樹在計算機科學中有很多應用,比如編譯器、IDE、壓縮優化代碼等。下面介紹一下抽象語法樹在 JavaScript 中的應用。

  JavaScript 抽象語法樹

構造 JavaScript 抽象語法樹有多種工具,比如 v8、SpiderMonkey、UglifyJS 等,這裏重點介紹 UglifyJS。

  UglifyJS

UglifyJS 是使用最廣的. JavaScript 壓縮工具之一,而且自身也是用 JavaScript 寫的,使用它的方法很簡單(需要 nodejs 環境):

首先全局安裝:

[sudo ]npm install -g uglify-js

然後就可以使用了:

uglifyjs -m -o

關於 UglifyJS 的用法這裏就不多介紹了,我們要做的是一些更有趣的事情。

  UglifyJS Tools

UglifyJS 提供了一些工具用於分析 JavaScript 代碼,包括:

parser,把 JavaScript 代碼解析成抽象語法樹

code generator,通過抽象語法樹生成代碼

mangler,混淆 JavaScript 代碼

scope analyzer,分析變量定義的工具

tree walker,遍歷樹節點

tree transformer,改變樹節點

  生成抽象語法樹

使用 UglifyJS 生成抽象語法樹很簡單:

首先安裝 UglifyJS 為 npm 包:

npm install uglify-js --save-dev

然後使用 parse 方法即可:

var UglifyJS = require('uglify-js');var ast = e('function sum(foo, bar){ return foo + bar; }');

這樣生成的 ast 即為那一段代碼的抽象語法樹。那麼我們怎麼使用呢?

  使用 mangler 壓縮代碼

使用 mangler 可以通過將局部變量都縮短成一個字符來壓縮代碼。

var UglifyJS = require('uglify-js');var ast = e('function sum(foo, bar){ return foo + bar; }');

re_out_scope();

le_names();

(t_to_string());// function sum(a,b){return a+b}

  使用 walker 遍歷抽象語法樹

使用 walker 可以遍歷抽象語法樹,這種遍歷是深度遍歷。

var UglifyJS = require('uglify-js');var ast = e('function sum(foo, bar){ return foo + bar; }');

re_out_scope();

(new Walker(function(node) {

(t_to_string());

}));/*

function sum(foo,bar){return foo+bar}

function sum(foo,bar){return foo+bar}

sum

foo

bar

return foo+bar

foo+bar

foo

bar

*/

UglifyJS 已經提供了直接壓縮代碼的腳本,walker 看上去貌似也沒啥用,那麼這些工具有什麼使用場景呢?

  抽象語法樹的應用

利用抽象語法樹重構 JavaScript 代碼

假如我們有重構 JavaScript 的需求,它們就派上用場啦。

下面考慮這樣一個需求:

我們知道,parseInt 用於將字符串變成整數,但是它有第二個參數,表示以幾進制識別字符串,若沒有傳第二個參數,則會自行判斷,比如:

parseInt('10.23'); // 10 轉換成正整數parseInt('10abc'); // 10 忽略其他字符parseInt('10', 10); // 10 轉換成十進制parseInt('10', 2); // 2 轉換成二進制parseInt('0123'); // 83 or 123 不同瀏覽器不一樣,低版本瀏覽器會轉換成八進制parseInt('0x11'); // 17 轉換成十六進制

因為有一些情況是和我們預期不同的,所以建議任何時候都加上第二個參數。

下面希望有一個腳本,查看所有 parseInt 有沒有第二個參數,沒有的話加上第二個參數 10,表示以十進制識別字符串。

使用 UglifyJS 可以實現此功能:

#! /usr/bin/env nodevar U2 = require("uglify-js");function replace_parseint(code) {

var ast = e(code); // accumulate `parseInt()` nodes in this array

var parseint_nodes = [];

(new Walker(function(node){

if (node instanceof _Call

&& t_to_string() === 'parseInt'

&& th === 1) {

parseint_(node);

}

})); // now go through the nodes backwards and replace code

for (var i = parseint_th; --i >= 0;) { var node = parseint_nodes[i]; var start_pos = ; var end_pos = os;

(new _Number({

value: 10

})); var replacement = t_to_string({ beautify: true });

code = splice_string(code, start_pos, end_pos, replacement);

} return code;

}function splice_string(str, begin, end, replacement) {

return tr(0, begin) + replacement + tr(end);

}// test itfunction test() {

if (foo) { parseInt('12342');

} parseInt('0012', 3);

}

(replace_parseint(ring()));/*

function test() {

if (foo) {

parseInt("12342", 10);

}

parseInt('0012', 3);

}

*/

在這裏,使用了 walker 找到 parseInt 調用的地方,然後檢查是否有第二個參數,沒有的話,記錄下來,之後根據每個記錄,用新的包含第二個參數的內容替換掉原內容,完成代碼的重構。

也許有人會問,這種簡單的情況,用正則匹配也可以方便的替換,幹嘛要用抽象語法樹呢?

答案就是,抽象語法樹是通過分析語法實現的,有一些正則無法(或者很難)做到的優勢,比如,parseInt() 整個是一個字符串,或者在註釋中,此種情況會被正則誤判:

var foo = 'parseInt("12345")';// parseInt("12345");

  抽象語法樹在美團中的應用

在美團前端團隊,我們使用 YUI 作為前端底層框架,之前面臨的一個實際問題是,模塊之間的依賴關係容易出現疏漏。比如:

('mod1', function(Y) {

('#button1')late('click');

(array, fn);

1 = function() {/**/};

}, '', {

requires: [ 'node', 'array-extras'

]

});

('mod2', function(Y) {

1(); // (uri, config);}, '', {

requires: [ 'mod1', 'io'

]

});

以上代碼定義了兩個模塊,其中 mod1 模擬點擊了一下 id 為 button1 的元素,執行了 ,然後定義了方法 1,最後聲明瞭依賴 node 和 array-extras;mod2 執行了 mod1 中定義的方法,而 被註釋了,最後聲明瞭依賴 mod1 和 io。

此處 mod1 出現了兩個常見錯誤,一個是 simulate 是 otype 上的方法,容易忘掉聲明依賴 node-event-simulate3,另一個是 y 上只有部分方法需要依賴 array-extras,故此處多聲明瞭依賴 array-extras4;mod2 中添加註釋後,容易忘記刪除原來寫的依賴 io。

故正確的依賴關係應該如下:

('mod1', function(Y) {

('#button1')late('click');

(array, fn);

1 = function() {/**/};

}, '', {

requires: [ 'node', 'node-event-simulate'

]

});

('mod2', function(Y) {

1(); // (uri, config);}, '', {

requires: [ 'mod1'

]

});

為了使模塊依賴關係的檢測自動化,我們創建了模塊依賴關係檢測工具,它利用抽象語法樹,分析出定義了哪些接口,使用了哪些接口,然後查找這些接口應該依賴哪些模塊,進而找到模塊依賴關係的錯誤,大致的過程如下:

  找到代碼中模塊定義()的部分

分析每個模塊內函數定義,變量定義,賦值語句等,找出符合要求(以 Y 開頭)的輸出接口(如 mod1 中的 1)

生成「接口 - 模塊」對應關係

分析每個模塊內函數調用,變量使用等,找出符合要求的輸入接口(如 mod2 中的 ,,1)

通過「接口 - 模塊」對應關係,找到此模塊應該依賴哪些其他模塊

分析 requires 中是否有錯誤

使用此工具,保證每次提交代碼時,依賴關係都是正確無誤的,它幫助我們實現了模塊依賴關係檢測的自動化。

  總結

抽象語法樹在計算機領域中應用廣泛,以上僅討論了抽象語法樹在 JavaScript 中的一些應用,期待更多的用法等着大家去嘗試和探索。

熱門標籤