豆皮粉儿们,大家好呀。愉快的五一节就这么过去了,假期有没有好好游玩一番呢。今天由清风慕竹给大家带来一篇《如何用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 双向的场景。

  1. 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。

  1. 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代码的热更新。

更多精彩内容,定制礼品图书赠送,高薪职位内推,微信搜索关注“豆皮范儿”