代码 parse 成 AST 之后,可以对 AST 进行 transform,然后 generate 成目标代码,这是转译器(transpiler)的流程,也可以对 AST 进行解释执行,这是解释器(interpreter)的流程。这一节,我们来基于 babel parser 来实现一个简单的 js 解释器。
v8 的编译流水线
v8 包括 4 部分,parser、ignation 解释器,JIT 编译器,还有 garbage collector(垃圾回收器)。
- parser 负责把源码 parse 成 AST。
- ignation 解释器负责把 AST 转成字节码,然后解释执行
- turbofan 可以把代码编译成机器码,直接执行
- gc 负责堆内存的垃圾回收
其实最早期的 v8 是没有字节码的,就是直接解释执行 AST:
这种直接解释执行 AST 的解释器叫做 tree walker 解释器,这一节,我们来实现一下这种 js 解释器。
实现 JS 解释器
思路分析
当 parser 把 源码 parse 成 AST 之后,其实已经能够拿到源码的各部分信息了,比如
const a = 1 + 2;
对应的 AST 是这样的
当我们处理到 BinarayExpression 节点,operator 是 +,会做加法运算,取左右两边的值相加。
当我们处理到 NumercLiteral 节点,是数字字面量,直接返回它的值(value)。
当我们处理到 Identifier 节点,是标识符,直接返回名字(name)。
当我们处理到 VariableDeclarator,我们就知道是一个变量声明语句,要在作用域 (scope)中放一个属性,属性名为 id 的值, 属性值为 init 的值。而 id 和 init 可以求出来。
就这样,我们就完成了这段代码的解释执行。
代码实现
先搭一个基本的结构:
const parser = require('@babel/parser');
const { codeFrameColumns } = require('@babel/code-frame');
const sourceCode = `
const a = 1 + 2;
`;
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous'
});
const evaluator = (function() {
const astInterpreters = {
Program (node, scope) {
node.body.forEach(item => {
evaluate(item, scope);
})
}
}
const evaluate = (node, scope) => {
try {
return astInterpreters[node.type](node, scope);
} catch(e) {
if (e && e.message && e.message.indexOf('astInterpreters[node.type] is not a function') != -1) {
console.error('unsupported ast type: ' + node.type);
console.error(codeFrameColumns(sourceCode, node.loc, {
highlightCode: true
}));
} else {
console.error(e.message);
console.error(codeFrameColumns(sourceCode, node.loc, {
highlightCode: true
}));
}
}
}
return {
evaluate
}
})();
const globalScope = {};
evaluator.evaluate(ast.program, globalScope);
我们定义了一个 evaluator,这个就是 AST 解释器。从根节点来执行,最外层是 File 节点,取 program 属性,Program 有 body 属性,是 AST 的数组,遍历执行。如果有不支持的节点类型,通过 code frame 来打印 AST 对应的代码,并且提示不支持。
创建一个全局作用域传入每个 evaluate 方法,用于作用域中变量的声明和取值。
然后我们来实现一下 VariableDeclarator 节点的解释,在 astInterpreters 添加一下节点的支持:
VariableDeclaration(node, scope) {
node.declarations.forEach((item) => {
evaluate(item, scope);
});
},
VariableDeclarator(node, scope) {
const declareName = evaluate(node.id);
if (scope[declareName]) {
throw Error('duplicate declare variable:' + declareName);
} else {
scope[declareName] = evaluate(node.init, scope);
}
},
ExpressionStatement(node, scope) {
return evaluate(node.expression, scope);
},
BinaryExpression(node, scope) {
const leftValue = evaluate(node.left, scope);
const rightValue = evaluate(node.right, scope);;
switch(node.operator) {
case '+':
return leftValue + rightValue;
case '-':
return leftValue - rightValue;
case '*':
return leftValue * rightValue;
case '/':
return leftValue / rightValue;
default:
throw Error('upsupported operator:' + node.operator);
}
},
Identifier(node, scope) {
return node.name;
},
NumericLiteral(node, scope) {
return node.value;
}
因为声明语句可能有 const a = 1, b=2;
的一条语句多个声明的情况在,所以这里是 VariableDeclaration 包含多个 VariableDeclarator 的结构。每一个声明要检查下作用域中有没有,如果有的话就报错,没有的话才可以声明。(这里是按照严格模式的规定来解释的)
BinaryExpression 根据 operator 的不同做不同的求值,只支持了 +、-、*、/ 四种运算。
执行完成后,我们打印一下 globalScope 看下效果。
发现 globalScope 中已经声明了一个变量 a,值为 3。
但我们肯定不能这样来查看结果,需要支持 console.log 的全局 api 和函数调用逻辑。
这个阶段的全部代码(也可以从 github 上下载下来看):
const parser = require('@babel/parser');
const { codeFrameColumns } = require('@babel/code-frame');
const sourceCode = `
const a = 1 + 2;
`;
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous'
});
const evaluator = (function() {
const astInterpreters = {
Program (node, scope) {
node.body.forEach(item => {
evaluate(item, scope);
})
},
VariableDeclaration(node, scope) {
node.declarations.forEach((item) => {
evaluate(item, scope);
});
},
VariableDeclarator(node, scope) {
const declareName = evaluate(node.id);
if (scope[declareName]) {
throw Error('duplicate declare variable:' + declareName);
} else {
scope[declareName] = evaluate(node.init, scope);
}
},
ExpressionStatement(node, scope) {
return evaluate(node.expression, scope);
},
BinaryExpression(node, scope) {
const leftValue = evaluate(node.left, scope);
const rightValue = evaluate(node.right, scope);;
switch(node.operator) {
case '+':
return leftValue + rightValue;
case '-':
return leftValue - rightValue;
case '*':
return leftValue * rightValue;
case '/':
return leftValue / rightValue;
default:
throw Error('upsupported operator:' + node.operator);
}
},
Identifier(node, scope) {
return node.name;
},
NumericLiteral(node, scope) {
return node.value;
}
}
const evaluate = (node, scope) => {
try {
return astInterpreters[node.type](node, scope);
} catch(e) {
if (e && e.message && e.message.indexOf('astInterpreters[node.type] is not a function') != -1) {
console.error('unsupported ast type: ' + node.type);
console.error(codeFrameColumns(sourceCode, node.loc, {
highlightCode: true
}));
} else {
console.error(e.message);
console.error(codeFrameColumns(sourceCode, node.loc, {
highlightCode: true
}));
}
}
}
return {
evaluate
}
})();
const globalScope = {};
evaluator.evaluate(ast.program, globalScope);
console.log(globalScope);
要支持函数调用,首先要支持作用域链,因为函数执行会生成一个新的作用域,并且会按照作用域链查找变量。
我们定义一个 scope 类:
class Scope {
constructor(parentScope) {
this.parent = parentScope;
this.declarations = [];
}
set(name, value) {
this.declarations[name] = value;
}
getLocal(name) {
return this.declarations[name];
}
get(name) {
let res = this.getLocal(name);
if (res === undefined && this.parent) {
res = this.parent.get(name);
}
return res;
}
has(name) {
return !!this.getLocal(name);
}
}
Scope 有 declarations 属性,代表这个 scope 中声明的变量,并且还有 parentScope 属性指向父 scope,通过 set 方法在作用域中声明变量,通过 getLocal 查找本作用域的变量,通过 get 方法支持按照作用域链不断向上查找变量。
有了这个方法以后,我们全局 scope 就变成了:
const globalScope = new Scope();
evaluator.evaluate(ast.program, globalScope);
我们往其中注入全局变量:
const globalScope = new Scope();
globalScope.set('console', {
log: function (...args) {
console.log(chalk.green(...args));
},
error: function (...args) {
console.log(chalk.red(...args));
},
warn: function (...args) {
console.log(chalk.orange(...args));
},
});
evaluator.evaluate(ast.program, globalScope);
这里我们通过 chalk 做了不同颜色的打印。
然后需要支持 CallExpression 的函数调用:
MemberExpression(node, scope) {
const obj = scope.get(evaluate(node.object));
return obj[evaluate(node.property)]
},
CallExpression(node, scope) {
const fn = evaluate(node.callee, scope);
const args = node.arguments.map(item => {
if (item.type === 'Identifier') {
return scope.get(item.name);
}
return evaluate(item, scope);
});
if(node.callee.type === 'MemberExpression') {
const obj = evaluate(node.callee.object, scope);
return fn.apply(obj, args);
} else {
return fn.apply(null, args);
}
}
console.log 是一个 MemberExpression,先从 scope 中把 object 属性对应的值取出来,然后再取改值的 property 对应的属性。
函数调用 CallExpression 需要先从 scope 取出 callee 对应的函数,然后处理参数,如果是标识符 Identifier 的话要从 scope 中取出对应的值,之后调用这个函数,传入参数。如果是 obj.xxx 的形式也就是调用部分是 MemberExpresion 的话则要绑定 this 为该 obj。
执行下看下效果:
能够打印正确的结果,我们的 JS 解释器已经支持函数调用了。
函数调用
现在我们只是在全局注入函数,如果用户想自定义函数呢,我们还需要支持函数声明语句 FunctionDeclaration 的解释。
FunctionDeclaration 其实也是往作用域中放了一个值,只不过这个值是一个函数,流程和变量声明 VariableDeclarator 的解释类似。
FunctionDeclaration(node, scope) {
const declareName = evaluate(node.id);
if (scope.get(declareName)) {
throw Error('duplicate declare variable:' + declareName);
} else {
scope.set(declareName, function(...args) {
const funcScope = new Scope();
funcScope.parent = scope;
node.params.forEach((item, index) => {
funcScope.set(item.name, args[index]);
});
funcScope.set('this', this);
return evaluate(node.body, funcScope);
});
}
},
函数会生成一个新的 scope,我们把函数接收到的参数按照声明的 params 的名字,依次设置在 scope 中,this 也设置到作用域中(其实作用域和 this 是平行的关系,都在执行上下文中,这里简单实现下),然后调用这个函数的时候,就可以在作用域中查找到这个函数,并传入参数执行了。
总结
前面的章节,我们学习了 AST,知道 AST 可以进行 transform 然后生成代码,这一节,我们解释执行了 AST,实现了一个简单的 JS 解释器,可以定义函数、变量,有作用域链,可以注入全局 api。
v8 最早的实现方式也是直接解释执行 AST,但是现在多了一层,会先转成字节码,然后再解释执行。但是解释执行的思路和 AST 的方式类似。
我们是用 js 解释的 js,所以 funciton 的 apply 方法、全局 api 等都可以直接用,实际上一般 js 引擎都是 c++ 写的,没有这些东西,所有的都要自己去实现,包括内存分配(堆、调用栈)、全局 api等。
搞懂这个案例之后,你会对 JS 代码的执行原理有更深入的认识。
(代码在这里,建议 git clone 下来通过 node 跑一下)