最近在研究 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/