- 相關(guān)推薦
抽象語(yǔ)法樹(shù)在JavaScript中的應用
抽象語(yǔ)法樹(shù)是什么?在 JavaScript 中該如何應用?下面YJBYS小編為大家講解!
在計算機科學(xué)中,抽象語(yǔ)法樹(shù)(abstract syntax tree 或者縮寫(xiě)為 AST),或者語(yǔ)法樹(shù)(syntax tree),是源代碼的抽象語(yǔ)法結構的樹(shù)狀表現形式,這里特指編程語(yǔ)言的源代碼。樹(shù)上的每個(gè)節點(diǎn)都表示源代碼中的一種結構。之所以說(shuō)語(yǔ)法是「抽象」的,是因為這里的語(yǔ)法并不會(huì )表示出真實(shí)語(yǔ)法中出現的每個(gè)細節。1
果然比較抽象,不如先看幾個(gè)例子:
抽象語(yǔ)法樹(shù)舉例
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 |
+-------+ +-----------------+ +-------+
*/
從上述兩個(gè)例子可以看出,抽象語(yǔ)法樹(shù)是將源代碼根據其語(yǔ)法結構,省略一些細節(比如:括號沒(méi)有生成節點(diǎn)),抽象成樹(shù)形表達。
抽象語(yǔ)法樹(shù)在計算機科學(xué)中有很多應用,比如編譯器、IDE、壓縮優(yōu)化代碼等。下面介紹一下抽象語(yǔ)法樹(shù)在 JavaScript 中的應用。
JavaScript 抽象語(yǔ)法樹(shù)
構造 JavaScript 抽象語(yǔ)法樹(shù)有多種工具,比如 v8、SpiderMonkey、UglifyJS 等,這里重點(diǎn)介紹 UglifyJS。
UglifyJS
UglifyJS 是使用最廣的 JavaScript 壓縮工具之一,而且自身也是用 JavaScript 寫(xiě)的,使用它的方法很簡(jiǎn)單(需要 nodejs 環(huán)境):
首先全局安裝:
[sudo ]npm install -g uglify-js
然后就可以使用了:
uglifyjs -m srcFileName.js -o destFileName.min.js
關(guān)于 UglifyJS 的用法這里就不多介紹了,我們要做的是一些更有趣的事情。
UglifyJS Tools
UglifyJS 提供了一些工具用于分析 JavaScript 代碼,包括:
parser,把 JavaScript 代碼解析成抽象語(yǔ)法樹(shù)
code generator,通過(guò)抽象語(yǔ)法樹(shù)生成代碼
mangler,混淆 JavaScript 代碼
scope analyzer,分析變量定義的工具
tree walker,遍歷樹(shù)節點(diǎn)
tree transformer,改變樹(shù)節點(diǎn)
生成抽象語(yǔ)法樹(shù)
使用 UglifyJS 生成抽象語(yǔ)法樹(shù)很簡(jiǎn)單:
首先安裝 UglifyJS 為 npm 包:
npm install uglify-js --save-dev
然后使用 parse 方法即可:
var UglifyJS = require('uglify-js');var ast = UglifyJS.parse('function sum(foo, bar){ return foo + bar; }');
這樣生成的 ast 即為那一段代碼的抽象語(yǔ)法樹(shù)。那么我們怎么使用呢?
使用 mangler 壓縮代碼
使用 mangler 可以通過(guò)將局部變量都縮短成一個(gè)字符來(lái)壓縮代碼。
var UglifyJS = require('uglify-js');var ast = UglifyJS.parse('function sum(foo, bar){ return foo + bar; }');
ast.figure_out_scope();
ast.mangle_names();
console.log(ast.print_to_string());// function sum(a,b){return a+b}
使用 walker 遍歷抽象語(yǔ)法樹(shù)
使用 walker 可以遍歷抽象語(yǔ)法樹(shù),這種遍歷是深度遍歷。
var UglifyJS = require('uglify-js');var ast = UglifyJS.parse('function sum(foo, bar){ return foo + bar; }');
ast.figure_out_scope();
ast.walk(new UglifyJS.TreeWalker(function(node) {
console.log(node.print_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 已經(jīng)提供了直接壓縮代碼的腳本,walker 看上去貌似也沒(méi)啥用,那么這些工具有什么使用場(chǎng)景呢?
抽象語(yǔ)法樹(shù)的應用
利用抽象語(yǔ)法樹(shù)重構 JavaScript 代碼
假如我們有重構 JavaScript 的需求,它們就派上用場(chǎng)啦。
下面考慮這樣一個(gè)需求:
我們知道,parseInt 用于將字符串變成整數,但是它有第二個(gè)參數,表示以幾進(jìn)制識別字符串,若沒(méi)有傳第二個(gè)參數,則會(huì )自行判斷,比如:
parseInt('10.23'); // 10 轉換成正整數parseInt('10abc'); // 10 忽略其他字符parseInt('10', 10); // 10 轉換成十進(jìn)制parseInt('10', 2); // 2 轉換成二進(jìn)制parseInt('0123'); // 83 or 123 不同瀏覽器不一樣,低版本瀏覽器會(huì )轉換成八進(jìn)制parseInt('0x11'); // 17 轉換成十六進(jìn)制
因為有一些情況是和我們預期不同的,所以建議任何時(shí)候都加上第二個(gè)參數。
下面希望有一個(gè)腳本,查看所有 parseInt 有沒(méi)有第二個(gè)參數,沒(méi)有的話(huà)加上第二個(gè)參數 10,表示以十進(jìn)制識別字符串。
使用 UglifyJS 可以實(shí)現此功能:
#! /usr/bin/env nodevar U2 = require("uglify-js");function replace_parseint(code) {
var ast = U2.parse(code); // accumulate `parseInt()` nodes in this array
var parseint_nodes = [];
ast.walk(new U2.TreeWalker(function(node){
if (node instanceof U2.AST_Call
&& node.expression.print_to_string() === 'parseInt'
&& node.args.length === 1) {
parseint_nodes.push(node);
}
})); // now go through the nodes backwards and replace code
for (var i = parseint_nodes.length; --i >= 0;) { var node = parseint_nodes[i]; var start_pos = node.start.pos; var end_pos = node.end.endpos;
node.args.push(new U2.AST_Number({
value: 10
})); var replacement = node.print_to_string({ beautify: true });
code = splice_string(code, start_pos, end_pos, replacement);
} return code;
}function splice_string(str, begin, end, replacement) {
return str.substr(0, begin) + replacement + str.substr(end);
}// test itfunction test() {
if (foo) { parseInt('12342');
} parseInt('0012', 3);
}
console.log(replace_parseint(test.toString()));/*
function test() {
if (foo) {
parseInt("12342", 10);
}
parseInt('0012', 3);
}
*/
在這里,使用了 walker 找到 parseInt 調用的地方,然后檢查是否有第二個(gè)參數,沒(méi)有的話(huà),記錄下來(lái),之后根據每個(gè)記錄,用新的包含第二個(gè)參數的內容替換掉原內容,完成代碼的重構。
也許有人會(huì )問(wèn),這種簡(jiǎn)單的情況,用正則匹配也可以方便的替換,干嘛要用抽象語(yǔ)法樹(shù)呢?
答案就是,抽象語(yǔ)法樹(shù)是通過(guò)分析語(yǔ)法實(shí)現的,有一些正則無(wú)法(或者很難)做到的優(yōu)勢,比如,parseInt() 整個(gè)是一個(gè)字符串,或者在注釋中,此種情況會(huì )被正則誤判:
var foo = 'parseInt("12345")';// parseInt("12345");
抽象語(yǔ)法樹(shù)在美團中的應用
在美團前端團隊,我們使用 YUI 作為前端底層框架,之前面臨的一個(gè)實(shí)際問(wèn)題是,模塊之間的依賴(lài)關(guān)系容易出現疏漏。比如:
YUI.add('mod1', function(Y) {
Y.one('#button1').simulate('click');
Y.Array.each(array, fn);
Y.mod1 = function() {/**/};
}, '', {
requires: [ 'node', 'array-extras'
]
});
YUI.add('mod2', function(Y) {
Y.mod1(); // Y.io(uri, config);}, '', {
requires: [ 'mod1', 'io'
]
});
以上代碼定義了兩個(gè)模塊,其中 mod1 模擬點(diǎn)擊了一下 id 為 button1 的元素,執行了 Y.Array.each,然后定義了方法 Y.mod1,最后聲明了依賴(lài) node 和 array-extras;mod2 執行了 mod1 中定義的方法,而 Y.io 被注釋了,最后聲明了依賴(lài) mod1 和 io。
此處 mod1 出現了兩個(gè)常見(jiàn)錯誤,一個(gè)是 simulate 是 Y.Node.prototype 上的方法,容易忘掉聲明依賴(lài) node-event-simulate3,另一個(gè)是 Y.Array 上只有部分方法需要依賴(lài) array-extras,故此處多聲明了依賴(lài) array-extras4;mod2 中添加注釋后,容易忘記刪除原來(lái)寫(xiě)的依賴(lài) io。
故正確的依賴(lài)關(guān)系應該如下:
YUI.add('mod1', function(Y) {
Y.one('#button1').simulate('click');
Y.Array.each(array, fn);
Y.mod1 = function() {/**/};
}, '', {
requires: [ 'node', 'node-event-simulate'
]
});
YUI.add('mod2', function(Y) {
Y.mod1(); // Y.io(uri, config);}, '', {
requires: [ 'mod1'
]
});
為了使模塊依賴(lài)關(guān)系的檢測自動(dòng)化,我們創(chuàng )建了模塊依賴(lài)關(guān)系檢測工具,它利用抽象語(yǔ)法樹(shù),分析出定義了哪些接口,使用了哪些接口,然后查找這些接口應該依賴(lài)哪些模塊,進(jìn)而找到模塊依賴(lài)關(guān)系的錯誤,大致的過(guò)程如下:
找到代碼中模塊定義(YUI.add)的部分
分析每個(gè)模塊內函數定義,變量定義,賦值語(yǔ)句等,找出符合要求(以 Y 開(kāi)頭)的輸出接口(如 mod1 中的 Y.mod1)
生成「接口 - 模塊」對應關(guān)系
分析每個(gè)模塊內函數調用,變量使用等,找出符合要求的輸入接口(如 mod2 中的 Y.one,Y.Array.each,Y.mod1)
通過(guò)「接口 - 模塊」對應關(guān)系,找到此模塊應該依賴(lài)哪些其他模塊
分析 requires 中是否有錯誤
使用此工具,保證每次提交代碼時(shí),依賴(lài)關(guān)系都是正確無(wú)誤的,它幫助我們實(shí)現了模塊依賴(lài)關(guān)系檢測的自動(dòng)化。
總結
抽象語(yǔ)法樹(shù)在計算機領(lǐng)域中應用廣泛,以上僅討論了抽象語(yǔ)法樹(shù)在 JavaScript 中的一些應用,期待更多的用法等著(zhù)大家去嘗試和探索。
【抽象語(yǔ)法樹(shù)在JavaScript中的應用】相關(guān)文章:
在Java中執行JavaScript代碼04-01
JavaScript中的with關(guān)鍵字03-25
整理Javascript基礎語(yǔ)法學(xué)習筆記欣賞04-01
perl- javascript中class的機制03-25
JavaScript中的三種對象04-01
javascript閉包的定義及應用實(shí)例分析04-01