JAVAScript黑科技:直接運行AST(抽象語法樹)
實現一個AST解釋器
一段JavaScript代碼,經過語法分析、語法分析等編譯過程之后,會形成一個對應的AST(抽象語法樹),形如:
AST是一個JSON格式的大字符串,包含有代碼相關信息,如:成員表達式調用、參數、標識符、字符串字面量等等。如:
{"type":"File","program":{"type":"Program","body":[{"type":"ExpressionStatement","expression":{"type":"CallExpression","callee":{"type":"MemberExpression","object":{"type":"Identifier","name":"console"},"property":{"type":"Identifier","name":"log"}},"arguments":[{"type":"StringLiteral","value":"jshaman"}]}}]}}
上面,是一段AST。
本文將要實現的目標的:直接運行這段AST。
先展示運行效果,如下:
即,運行后,輸出一了個字符串。
功能意義
其實,如果直接要輸出這樣一個字符串,在JavaScript中是極為簡單的,只需簡單的一句:console.log('jshaman')。
那么,為什么要轉化為復雜的AST,再執行AST呢?
其意義在于:我們將要實現一個AST解釋器,引申而言,實現一個JavaScript解釋器。在很多場景中,具有非常實用的意義。
比如,在小程序中屏蔽了Eval函數,而如果我們自己實現解釋器,將可突破這個限制。
又比如,JShaman研發團隊中,將它用于JavaScript代碼加密。
Tip:JShaman是國內專業的JavaScript代碼保護研究團隊,擁有眾多自主的JS代碼加密方案,此為其一。
實現方案
要讓這個AST能被執行,即要依JavaScript代碼標準解釋AST。
首先,嘗試理解console.log('jshaman')這句代碼的AST。通過astexplorer查看:
可以看到,這一句代碼轉成的AST中,包含7個節點。
那么,要執行這個AST,就要能正確處理這7種節點類型。
由于AST是JSON結構,處理時,可遍歷其所有的成員節點。參考astexplorer展示的節點,分別處理:File、Program、ExpressionStatement、CallExpression等,代碼如下:
當遇到CallExpression時,獲取其對應的參數、方法名等,如下圖:
并用Apply的方式進行執行,以返回結果。
原理即如此。
編碼時,對照著AST節點類型,完成相應的操作即可,為方便調試,可輸出節點類型加以分析,如下圖:
完整源碼
完整源碼如下,保存為JS,在NodeJS環境中即可運行。也可在瀏覽器中直接運行代碼,更為方便。
//各節點處理器
var visitors = {
//File節點,JS代碼AST的根節點
File(node, scope) {
ast_excute(node.program, scope);
},
//File的次節點,其Body下對應各行JS代碼
Program(program, scope) {
for (i=0; i< program.body.length;i++) {
//執行各行代碼的AST
ast_excute(program.body[i], scope);
}
},
//Call調用AST之外會包裹有一層表達式語句結構
ExpressionStatement(node, scope) {
return ast_excute(node.expression, scope);
},
//Call調用
CallExpression(node, scope){
//遍歷callee獲取函數體
var func = ast_excute(node.callee, scope);
//獲取參數
var args = node.arguments.map( function(arg){
return ast_excute(arg, scope)
});
var value;
if (node.callee.type === 'MemberExpression') {
value = ast_excute(node.callee.object, scope);
}
//返回函數運行結果
return func.apply(value, args)
},
//成員表達式
MemberExpression(node, scope){
//獲取對象,如console
var obj = ast_excute(node.object, scope);
//獲取對象的方法,如log
var name = node.property.name
//返回表達式,如console.log
return obj[name]
},
//標識符
Identifier(node, scope) {
return scope[node.name];
},
//字符串字面量
StringLiteral(node) {
return node.value;
},
};
//執行
function ast_excute(node, scope) {
var _evalute = visitors[node.type];
if (!_evalute) {
throw new Error("未知的AST類型:" , node.type);
}
// 遞歸調用
return _evalute(node, scope);
}
var ast = {"type":"File","program":{"type":"Program","body":[{"type":"ExpressionStatement","expression":{"type":"CallExpression","callee":{"type":"MemberExpression","object":{"type":"Identifier","name":"console"},"property":{"type":"Identifier","name":"log"}},"arguments":[{"type":"StringLiteral","value":"jshaman"}]}}]}};
ast_excute(ast, {console});
AST簡化
以上代碼中,使用的是簡化過的AST。astexplorer默認生成的AST,內容較多,如下圖:
其包含有代碼行號、起始、結束等位置信息:
但這些冗長的位置信息對于執行是無用的,可以將其去除,實現簡化的AST:
這樣就成為了代碼中使用的、較簡短的AST。