最近在研究 AST, 以前有一篇文章 面試官: 你瞭解過 Babel 嗎?寫過 Babel 插件嗎? 答: 沒有。卒 爲何要去了解它? 由於懂得 AST 真的能夠隨心所欲javascript
簡單點說,使用 Javascript 運行Javascript代碼。java
這篇文章來告訴你,如何寫一個最簡單的解析器。node
在你們的認知中,有幾種執行自定義腳本的方法?咱們來列舉一下:git
function runJavascriptCode(code) {
const script = document.createElement("script");
script.innerText = code;
document.body.appendChild(script);
}
runJavascriptCode("alert('hello world')");
複製代碼
無數人都在說,不要使用eval
,雖然它能夠執行自定義腳本es6
eval("alert('hello world')");
複製代碼
參考連接: Why is using the JavaScript eval function a bad idea?github
setTimeout 一樣能執行,不過會把相關的操做,推到下一個事件循環中執行面試
setTimeout("console.log('hello world')");
console.log("I should run first");
// 輸出
// I should run first
// hello world'
複製代碼
new Function("alert('hello world')")();
複製代碼
參考連接: Are eval() and new Function() the same thing?express
能夠把 Javascript 代碼寫進一個 Js 文件,而後在其餘文件 require 它,達到執行的效果。小程序
NodeJs 會緩存模塊,若是你執行 N 個這樣的文件,可能會消耗不少內存. 須要執行完畢後,手動清除緩存。緩存
const vm = require("vm");
const sandbox = {
animal: "cat",
count: 2
};
vm.runInNewContext('count += 1; name = "kitty"', sandbox);
複製代碼
以上方式,除了 Node 能優雅的執行之外,其餘都不行,API 都須要依賴宿主環境。
在能任何執行 Javascript 的代碼的平臺,執行自定義代碼。
好比小程序,屏蔽了以上執行自定義代碼的途徑
那就真的不能執行自定義代碼了嗎?
非也
基於 AST(抽象語法樹),找到對應的對象/方法, 而後執行對應的表達式。
這怎麼說的有點繞口呢,舉個栗子console.log("hello world");
原理: 經過 AST 找到console
對象,再找到它log
函數,最後運行函數,參數爲hello world
咱們以運行console.log("hello world")
爲例
打開astexplorer, 查看對應的 AST
由圖中看到,咱們要找到console.log("hello world")
,必需要向下遍歷節點的方式,通過File
、Program
、ExpressionStatement
、CallExpression
、MemberExpression
節點,其中涉及到Identifier
、StringLiteral
節點
咱們先定義visitors
, visitors
是對於不一樣節點的處理方式
const visitors = {
File(){},
Program(){},
ExpressionStatement(){},
CallExpression(){},
MemberExpression(){},
Identifier(){},
StringLiteral(){}
};
複製代碼
再定義一個遍歷節點的函數
/** * 遍歷一個節點 * @param {Node} node 節點對象 * @param {*} scope 做用域 */
function evaluate(node, scope) {
const _evalute = visitors[node.type];
// 若是該節點不存在處理函數,那麼拋出錯誤
if (!_evalute) {
throw new Error(`Unknown visitors of ${node.type}`);
}
// 執行該節點對應的處理函數
return _evalute(node, scope);
}
複製代碼
下面是對各個節點的處理實現
const babylon = require("babylon");
const types = require("babel-types");
const visitors = {
File(node, scope) {
evaluate(node.program, scope);
},
Program(program, scope) {
for (const node of program.body) {
evaluate(node, scope);
}
},
ExpressionStatement(node, scope) {
return evaluate(node.expression, scope);
},
CallExpression(node, scope) {
// 獲取調用者對象
const func = evaluate(node.callee, scope);
// 獲取函數的參數
const funcArguments = node.arguments.map(arg => evaluate(arg, scope));
// 若是是獲取屬性的話: console.log
if (types.isMemberExpression(node.callee)) {
const object = evaluate(node.callee.object, scope);
return func.apply(object, funcArguments);
}
},
MemberExpression(node, scope) {
const { object, property } = node;
// 找到對應的屬性名
const propertyName = property.name;
// 找對對應的對象
const obj = evaluate(object, scope);
// 獲取對應的值
const target = obj[propertyName];
// 返回這個值,若是這個值是function的話,那麼應該綁定上下文this
return typeof target === "function" ? target.bind(obj) : target;
},
Identifier(node, scope) {
// 獲取變量的值
return scope[node.name];
},
StringLiteral(node) {
return node.value;
}
};
function evaluate(node, scope) {
const _evalute = visitors[node.type];
if (!_evalute) {
throw new Error(`Unknown visitors of ${node.type}`);
}
// 遞歸調用
return _evalute(node, scope);
}
const code = "console.log('hello world')";
// 生成AST樹
const ast = babylon.parse(code);
// 解析AST
// 須要傳入執行上下文,不然找不到``console``對象
evaluate(ast, { console: console });
複製代碼
在 Nodejs 中運行試試看
$ node ./index.js
hello world
複製代碼
而後咱們更改下運行的代碼 const code = "console.log(Math.pow(2, 2))";
由於上下文沒有Math
對象,那麼會得出這樣的錯誤 TypeError: Cannot read property 'pow' of undefined
記得傳入上下文evaluate(ast, {console, Math});
再運行,又得出一個錯誤Error: Unknown visitors of NumericLiteral
原來Math.pow(2, 2)
中的 2,是數字字面量
節點是NumericLiteral
, 可是在visitors
中,咱們卻沒有定義這個節點的處理方式.
那麼咱們就加上這麼個節點:
NumericLiteral(node){
return node.value;
}
複製代碼
再次運行,就跟預期結果一致了
$ node ./index.js
4
複製代碼
到這裏,已經實現了最最基本的函數調用了
既然是解釋器,難道只能運行 hello world 嗎?顯然不是
var name = "hello world";
console.log(name);
複製代碼
先看下 AST 結構
visitors
中缺乏VariableDeclaration
和VariableDeclarator
節點的處理,咱們給加上
VariableDeclaration(node, scope) {
const kind = node.kind;
for (const declartor of node.declarations) {
const {name} = declartor.id;
const value = declartor.init
? evaluate(declartor.init, scope)
: undefined;
scope[name] = value;
}
},
VariableDeclarator(node, scope) {
scope[node.id.name] = evaluate(node.init, scope);
}
複製代碼
運行下代碼,已經打印出hello world
function test() {
var name = "hello world";
console.log(name);
}
test();
複製代碼
根據上面的步驟,新增了幾個節點
BlockStatement(block, scope) {
for (const node of block.body) {
// 執行代碼塊中的內容
evaluate(node, scope);
}
},
FunctionDeclaration(node, scope) {
// 獲取function
const func = visitors.FunctionExpression(node, scope);
// 在做用域中定義function
scope[node.id.name] = func;
},
FunctionExpression(node, scope) {
// 本身構造一個function
const func = function() {
// TODO: 獲取函數的參數
// 執行代碼塊中的內容
evaluate(node.body, scope);
};
// 返回這個function
return func;
}
複製代碼
而後修改下CallExpression
// 若是是獲取屬性的話: console.log
if (types.isMemberExpression(node.callee)) {
const object = evaluate(node.callee.object, scope);
return func.apply(object, funcArguments);
} else if (types.isIdentifier(node.callee)) {
// 新增
func.apply(scope, funcArguments); // 新增
}
複製代碼
運行也能過打印出hello world
限於篇幅,我不會講怎麼處理全部的節點,以上已經講解了基本的原理。
對於其餘節點,你依舊能夠這麼來,其中須要注意的是: 上文中,做用域我統一用了一個 scope,沒有父級/子級做用域之分
也就意味着這樣的代碼是能夠運行的
var a = 1;
function test() {
var b = 2;
}
test();
console.log(b); // 2
複製代碼
處理方法: 在遞歸 AST 樹的時候,遇到一些會產生子做用域的節點,應該使用新的做用域,好比說function
,for in
等
以上只是一個簡單的模型,它連玩具都算不上,依舊有不少的坑。好比:
連續幾個晚上的熬夜以後,我寫了一個比較完善的庫vm.js,基於jsjs修改而來,站在巨人的肩膀上。
與它不一樣的是:
目前正在開發中, 等待更加完善以後,會發布第一個版本。
歡迎大佬們拍磚和 PR.
小程序從此變成大程序,業務代碼經過 Websocket 推送過來執行,小程序源碼只是一個空殼,想一想都刺激.
項目地址: github.com/axetroy/vm.…
在線預覽: axetroy.github.io/vm.js/