豆皮粉儿们,你们好呀。愉快的五一节就这么过去了,假期有没有好好游玩一番呢。今天由清风慕竹给你们带来一篇《如何用go实现一个js解释器》。node
做者:清风慕竹c++
前段时间在开发版本发布系统过程当中,为了追求系统的灵活性,咱们容许用户经过写js的方式生成json配置,业务上有定制的需求能够经过js代码来实现,这样在不调整底层系统的状况下能够尽量的支持业务中的个性化需求。因为发布系统是用golang开发的,因此这里须要一个go版本的js解释器(不须要考虑gc、jit、inline-cache等复杂内容,只是一个简单的解释器的实现,能够解析并执行js便可),可以在golang应用中安全的运行js
代码。git
关于js解释器的实现其实已经有不少版本了,好比tinyjs(c++版本的实现)、tinyjs.py(py版本的实现)、还有若干用js自举实现的版本,好比eval5 。这些解释器实现思路大体以下:github
其中转换步骤是可选的,这一步主要工做是将语法树上的节点转换成目标语言可执行的节点,对于eval5这种js-in-js的实现,这一步就不须要实现了。可是对js-in-x(x多是go、c++、py)这种状况则须要增长转换的步骤。关于词法分析、语法解析这两块实现资料比较多,这里再也不赘述,熟悉js的同窗能够参考acorn、babel-parser、espree等实现,这里重点讲下转换和遍历执行的过程。golang
在转换、执行以前须要先解决go和js数据交换的问题,须要考虑 js< --- >go 双向的场景。express
js代码在ast语法树转换的过程当中,对应的ast节点转换的过程当中被转换成expression节点,基本的值被装箱成Value类型,golang访问js变量实际上访问的是变量对应的ast节点转换后生成的expression节点。 好比在js中定义以下:json
var a = 2;
function print(name) {
console.log('hello ' + name)
}
复制代码
变量定义转换以下:数组
函数定义转换以下:安全
golang在执行前会处理变量定义,处理以后会在对应做用域对象上生成key(变量名or函数名)到expression的binding:微信
go里面并不会直接访问js变量,而是访问js变量对应的expression。
假定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对应的值。函数也是如此。
从ast树的body节点开始遍历,依次执行statement转换的过程
好比上图中 1+1
在ast树上对应的节点是ExpressionStatement,对应的会依次调用parseStatement:
parseExpression:
ExpressionStatement内部的expression是BinaryExpression,最终会转换成以下结构:
_nodeBinaryExpression {
operator: token.PLUS,
comparison: false,
left: &_nodeLiteral{
value: Value{
kind: valueNumber,
value: 1,
},
},
right: &_nodeLiteral{
value: Value{
kind: valueNumber,
value: 1,
},
},
}
复制代码
处理变量声明状况
处理变量声明和js的变量提高相关,在遍历完ast树以后,对于树上的变量、函数的定义,会保存到varList、functionList数组中。
1+1
为例:
遍历ast执行对应的节点时须要注意,js存在做用域的区别(全局做用域、函数做用域)。在上面的代码执行的时候,默认将代码放在全局做用域执行:
enterGlobalScope和leaveScope对应实现以下:
进入globalScope的时候会把当前runtime的scope暂存在_scope.outer字段上,defer对应的匿名函数在函数执行完毕以后执行,当函数执行完以后,再把scope.outers上暂存的scope恢复回来。globalScope和functionScope的scope对象是隔离的,在非严格模式下,functionScope会是一个从globalScope深拷贝的对象。
接下来处理变量定义,好比var a = 1
变量定义 或者function f(){}
函数定义,变量和函数存放的地方在当前做用域scope对象上的variable.object.property字段上,这个字段其实类型就是一个巨型map,存储的时候key为变量名,值为js对应值的包装类型。完成变量、函数定义后,遍历program下的body节点,并根据节点类型执行对应的操做:
接着执行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代码的热更新。
更多精彩内容,定制礼品图书赠送,高薪职位内推,微信搜索关注“豆皮范儿”