解释器
解释器,就是遍历AST语法树,然后根据Node节点类型,去执行或计算每个节点。
这里实现一个JS解释器,需要对AST语法树Node节点每个类型做区分判断,主要有以下几种:
- 变量
- 作用域以及作用域链
- 上下文This指向
- 条件判断
- For循环,其中的 break和continue
- 函数部分 Function
- 生成器 Generator
- 异步 Async
因此我们需要几个类去保存相关的值:
- Scope,作用域类,保存作用域内的值以及作用域链(当前作用域可以找到父级作用域链)
- Visitor,AST树Node节点处理类,里面有函数
visitNode(node, scope)
,用来处理对应node类型,其中VISITOR是所有类型函数的Map对象,用来快速查询 - Variable,变量存储类,用来存储变量类型和值
参考代码:
/** * 遍历AST语法树,并执行对应的处理函数 * @param {*} node * @param {*} scope */ visitNode(node, scope) { const { type } = node; if (VISITOR[type]) { return VISITOR[type]({ node, scope, context: this }); } return undefined; }
变量和作用域
在JavaScript中,对变量的声明通常是绑定在作用域中的,而作用域分为以下几种:
- 全局作用域,全局作用域中仅存在一处,即为最上级的环境。
- 函数作用域,函数存在并执行时,内部存储函数作用域
- 块级作用域,每隔block块{}都可产生作用域,如if for while等
我们举个例子,以var a = 1;
为例,我们需要需要哪些代码,才能实现从AST树解析,将变量a
被声明在全局变量作用域中,具体步骤如下图:
这里实现一个作用域Scope类,参考代码如下:
class Scope { /** * * @param {*} type * @param {*} parent */ constructor(type, parent) { this.parent = parent || null; // 父级作用域 this.type = type; // 作用域类型 Global, Function, Block this.targetScope = new Map(); // 当前作用域 } /** * 变量声明方法,变量已定义则抛出语法错误异常 * @param {*} kind 变量类型 * @param {*} rawName 变量名 * @param {*} value 变量值 * @returns */ declare(kind, rawName, value) { this.targetScope.set(rawName, value); } }
上下文This
上下文的this对象其实指的就是当前作用域,然而我们了解过JS中的this是可以改变的,如:
call()
bind()
apply()
等函数,当执行到相关函数的时候,需要将传递进来scope的替换成当前的scope- ES6中的箭头函数等,this指向上一级
这些都需要在解析代码的时候注意的逻辑问题。
其他类型解释
条件判断
IfStatement
,里面有属性: test
为判断条件,consequent
为条件成立时执行的语句,alternate
为条件不成立时执行的语句,参考代码如下:
// visitNode会执行AST语法树节点函数 const { test, consequent, alternate } = node; const testValue = visitNode(test, scope); if (testValue) { if(consequent){ visitNode(consequent, scope); } } else { if(alternate){ visitNode(alternate, scope); } }
其他部分逻辑就不会在这里一一描述,具体Node类型都有自己的判断逻辑,因此想要了解完整逻辑,可以到完整源码里查看,注解都十分清晰。
完整源码地址在:github.com/qiubohong/q…
总结
本文涉及的东西有点多,花了好几天时间才弄明白,因此有些知识点在这里做一下小总结:
- JS引擎是有三部分组成的,分别是:
词法分析
,语法解析
和解释器
- 词法解析和语法解析,最终的目标是生成符合ESTree规范的
AST语法树
- 解释器的作用就是依据
AST语法树
去执行相关逻辑,输出所需要的最终结果
- 比较重要的部分在于变量、作用域和作用域链的实现
- 其他部分则是依据对应
ECMAScript 规范
实现对应逻辑皆可
ECMAScript 规范
其实,JS解释器实现起来不难,就是需要对JS执行逻辑有完整的认识,不仅仅只是上面几个部分,但是基本上AST语法树都已经包括在里面了,所以这个时候需要你对ECMAScript 规范有一定了解才能完整实现解释器。
所以这里贴一个ECMAScript 规范链接,作为后续完整解释器的扩展。
使用 ECMAScript 指代由 Ecma International Technical Committee 39 负责编撰的 ECMAScript Language Specification,而使用 JavaScript 来指代我们日常使用的那个常见编程语言。
我们可以在 tc39.es/ecma262 获取到最新的 ECMAScript 规范
如何阅读规范呢?
- 惯例与基础:如在 ECMAScript 中 Number 的定义是什么,亦或者 throw a TypeError exception 语句代表什么含义;
- 语言语法产生式:如如何写一个符合规范的 for-in 循环;
- 语言静态语义:如一个 VariableDeclaration 如何确定一个变量声明;
- 语言运行时语义:如一个 for-in 循环的执行例程的定义;
- APIs:如 String.prototype.substring 等内置对象的方法例程定义。
能做什么
有了我们自己的JS引擎后我们能做些什么,其实目前市面上已经有很多应用场景,比如:
- Babel,最常见JavaScript编译器,能够将js代码编译成我们想要的任一版本的ECMAscript标准,基于 acorn.js优化
- ESlint,最常用的代码质量扫描工具,能找到代码不符合规范的地方,基于Espree.js进行分析
- pretiier,最常用的代码格式工具,能帮忙把代码格式优化
- js沙箱安全机制,之前写过一篇文件低代码系列——js沙箱设计里提到过,想要完整动态执行js代码,最好的方式是拥有自己的js解释器