Skip to content

代码 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 负责堆内存的垃圾回收

image.png

其实最早期的 v8 是没有字节码的,就是直接解释执行 AST:

image.png

这种直接解释执行 AST 的解释器叫做 tree walker 解释器,这一节,我们来实现一下这种 js 解释器。

实现 JS 解释器

思路分析

当 parser 把 源码 parse 成 AST 之后,其实已经能够拿到源码的各部分信息了,比如

javascript
const a = 1 + 2;

对应的 AST 是这样的

image.png

当我们处理到 BinarayExpression 节点,operator 是 +,会做加法运算,取左右两边的值相加。

当我们处理到 NumercLiteral 节点,是数字字面量,直接返回它的值(value)。

当我们处理到 Identifier 节点,是标识符,直接返回名字(name)。

当我们处理到 VariableDeclarator,我们就知道是一个变量声明语句,要在作用域 (scope)中放一个属性,属性名为 id 的值, 属性值为 init 的值。而 id 和 init 可以求出来。

就这样,我们就完成了这段代码的解释执行。

代码实现

先搭一个基本的结构:

javascript
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 添加一下节点的支持:

javascript
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 看下效果。

image.png

发现 globalScope 中已经声明了一个变量 a,值为 3。

但我们肯定不能这样来查看结果,需要支持 console.log 的全局 api 和函数调用逻辑。

这个阶段的全部代码(也可以从 github 上下载下来看):

javascript
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 类:

javascript
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 就变成了:

javascript
const globalScope = new Scope();

evaluator.evaluate(ast.program, globalScope);

我们往其中注入全局变量:

javascript
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 的函数调用:

javascript
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。

执行下看下效果:

image.png

能够打印正确的结果,我们的 JS 解释器已经支持函数调用了。

函数调用

现在我们只是在全局注入函数,如果用户想自定义函数呢,我们还需要支持函数声明语句 FunctionDeclaration 的解释。

FunctionDeclaration 其实也是往作用域中放了一个值,只不过这个值是一个函数,流程和变量声明 VariableDeclarator 的解释类似。

javascript
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 是平行的关系,都在执行上下文中,这里简单实现下),然后调用这个函数的时候,就可以在作用域中查找到这个函数,并传入参数执行了。

image.png

总结

前面的章节,我们学习了 AST,知道 AST 可以进行 transform 然后生成代码,这一节,我们解释执行了 AST,实现了一个简单的 JS 解释器,可以定义函数、变量,有作用域链,可以注入全局 api。

v8 最早的实现方式也是直接解释执行 AST,但是现在多了一层,会先转成字节码,然后再解释执行。但是解释执行的思路和 AST 的方式类似。

我们是用 js 解释的 js,所以 funciton 的 apply 方法、全局 api 等都可以直接用,实际上一般 js 引擎都是 c++ 写的,没有这些东西,所有的都要自己去实现,包括内存分配(堆、调用栈)、全局 api等。

搞懂这个案例之后,你会对 JS 代码的执行原理有更深入的认识。

(代码在这里,建议 git clone 下来通过 node 跑一下)