豆皮粉儿们,大家好呀。愉快的五一节就这么过去了,假期有没有好好游玩一番呢。今天由清风慕竹给大家带来一篇《如何用go实现一个js解释器》。
背景
js
实现思路
关于js解释器的实现其实已经有很多版本了,比如tinyjs(c++版本的实现)、tinyjs.py(py版本的实现)、还有若干用js自举实现的版本,比如eval5 。这些解释器实现思路大致如下:
其中转换步骤是可选的,这一步主要工作是将语法树上的节点转换成目标语言可执行的节点,对于eval5这种js-in-js的实现,这一步就不需要实现了。但是对js-in-x(x可能是go、c++、py)这种情况则需要增加转换的步骤。关于词法分析、语法解析这两块实现资料比较多,这里不再赘述,熟悉js的同学可以参考acorn、babel-parser、espree等实现,这里重点讲下转换和遍历执行的过程。
go与js数据交换在转换、执行之前需要先解决go和js数据交换的问题,需要考虑 js< — >go 双向的场景。
- go代码访问js变量
js代码在ast语法树转换的过程中,对应的ast节点转换的过程中被转换成expression节点,基本的值被装箱成Value类型,golang访问js变量实际上访问的是变量对应的ast节点转换后生成的expression节点。
比如在js中定义如下:
var a = 2;
function print(name) {
console.log('hello ' + name)
}
变量定义转换如下:
函数定义转换如下:
golang在执行前会处理变量定义,处理之后会在对应作用域对象上生成key(变量名or函数名)到expression的binding:
go里面并不会直接访问js变量,而是访问js变量对应的expression。
- js代码访问go变量
假定go提前注册了变量x和函数twoPlus:
vm := New()
vm.Set("x", 10)
vm.Set("twoPlus", func(call FunctionCall) Value {
right, _ := call.Argument(0).ToInteger()
result, _ := vm.ToValue(2 + right)
return result
})
js访问golang中变量x、函数twoPlus:
var a = x + 2;
var b = twoPlus(a)
console.log('twoPlus(a): ' + b)
js代码并没有直接执行,真正执行的是js代码对应的ast转换后的结果,golang侧注册变量实际上是把变量注册到了当前作用域的property上了:
执行ast转换后的节点时发现需要获取identifier x对应值的时候,会从property对应的map上拿到x对应的值。函数也是如此。
转换_nodeBinaryExpression {
operator: token.PLUS,
comparison: false,
left: &_nodeLiteral{
value: Value{
kind: valueNumber,
value: 1,
},
},
right: &_nodeLiteral{
value: Value{
kind: valueNumber,
value: 1,
},
},
}
遍历、执行以
1+1
遍历ast执行对应的节点时需要注意,js存在作用域的区别(全局作用域、函数作用域)。在上面的代码执行的时候,默认将代码放在全局作用域执行:
enterGlobalScope和leaveScope对应实现如下:
进入globalScope的时候会把当前runtime的scope暂存在_scope.outer字段上,defer对应的匿名函数在函数执行完毕之后执行,当函数执行完之后,再把scope.outers上暂存的scope恢复回来。globalScope和functionScope的scope对象是隔离的,在非严格模式下,functionScope会是一个从globalScope深拷贝的对象。
var a = 1function f(){}
接着执行ExpressionStatement中的expression,对应类型是BinaryExpression:
BinaryExpression需要计算左值和右值,先计算node.left:
再进入cmp_evaluate_nodeExpression时,此时expression类型为nodeLiteral, 直接返回node.value即可。下面根据node.operator执行对应的计算逻辑(参与的时候先对Value类型执行拆箱,获取基本类型的值后转换成float64类型参与计算,计算的结果以Value的包装类型返回):
1+1
总结
借助现有的js代码解析库可以相对容易的实现一个js的解释器,实现思路比较明确,但是对于新语法规范支持度还是比较差,后面可以进一步扩充语法。除了本文介绍的js-in-go的实现之外,js-in-js也有一些比较有意思的玩法,比如借助js-in-js的实现,可以在js引擎屏蔽了eval、new Function情况下实现js代码的热更新。
更多精彩内容,定制礼品图书赠送,高薪职位内推,微信搜索关注“豆皮范儿”