1.什么是 goja
Goja 是 ECMAScript 5.1 的纯 Go 实现,强调标准合规性和性能,项目很大程度上受到 otto 的启发,而 otto 包是一个用 Go 原生编写的 JavaScript 解析器和解释器。Goja 所需的最低 Go 版本是 1.16。
ECMAScript/JavaScript engine in pure Go
Goja 的典型特征包括:
- 完整的 ECMAScript 5.1 支持(包括正则表达式和严格模式)。
- 通过了迄今为止实现的功能的几乎所有 tc39 测试。
- 能够运行 Babel、Typescript 编译器和几乎任何用 ES5 编写的东西。
- 支持 Sourcemaps
- 大多数 ES6 功能仍在进行中,可以参阅 https://github.com/dop251/goja/milestone/1?close=1
目前 Goja 在 Github 上通过 MIT 协议开源,有超过 4.7k 的 star,7.2k 的项目依赖量,是一个值得关注的前端开源项目。
2.goja 基本用法
运行 JavaScript 并获取结果值
vm := goja.New()v, err := vm.RunString("2 + 2")if err != nil { panic(err)}if num := v.Export().(int64); num != 4 { panic(num)}
从 JS 导出值
可以使用 Value.Export() 方法将 JS 值导出为其默认的 Go 表示形式。或者,可以使用 Runtime.ExportTo() 方法将其导出到特定的 Go 变量中。
var fn func(string) stringerr = vm.ExportTo(vm.Get("f"), &fn)if err != nil { panic(err)}
在单个导出操作中,相同的对象将由相同的 Go 值(相同的映射、切片或指向相同结构的指针)表示,这包括 circular objects 并使得导出它们成为可能。
从 Go 调用 JS 函数
有两种方法,第一种方法是使用 AssertFunction():
const SCRIPT = `function sum(a, b) { return +a + b;}`vm := goja.New()_, err := vm.RunString(SCRIPT)if err != nil { panic(err)}sum, ok := goja.AssertFunction(vm.Get("sum"))if !ok { panic("Not a function")}res, err := sum(goja.Undefined(), vm.ToValue(40), vm.ToValue(2))if err != nil { panic(err)}fmt.Println(res)// Output: 42
当然,还可以使用 Runtime.ExportTo():
const SCRIPT = `function sum(a, b) { return +a + b;}`vm := goja.New()_, err := vm.RunString(SCRIPT)if err != nil { panic(err)}var sum func(int, int) interr = vm.ExportTo(vm.Get("sum"), &sum)if err != nil { panic(err)}fmt.Println(sum(40, 2))// note, _this_ value in the function will be undefined.// Output: 42
第一个函数的更加偏底层,允许指定该值,而第二个函数使该函数看起来像普通的 Go 函数。
3.goja 已知不兼容性和警告
WeakMap
WeakMap 是通过将对值的引用嵌入到键中来实现的, 这意味着只要键是可访问的,任何 WeakMap 中与其关联的所有值也仍然是可访问的,因此即使没有以其他方式引用,或者即使在 WeakMap 消失之后也无法进行垃圾收集。 当从 WeakMap 中显式删除该键或该键变得无法访问时,对该值的引用将被删除。
var m = new WeakMap();var key = {};var value = {/* 很大的对象 */};m.set(key, value);value = undefined;m = undefined;// 此时该值不会变成可垃圾回收的key = undefined;// 可以回收// m.delete(key);// 也可以垃圾回收
其原因是 Go 运行时的限制。 在撰写本文时(版本 1.15),在作为引用循环一部分的对象上设置终结器会使整个循环不可垃圾收集。 上面的解决方案是我能想到的唯一合理的方法,而不涉及终结器。
请注意,这不会对应用程序逻辑产生任何影响,但可能会导致内存使用量高于预期。
WeakRef 和 FinalizationRegistry
由于上述原因,现阶段实现 WeakRef 和 FinalizationRegistry 似乎是不可能的。
JSON
JSON.parse() 使用以 UTF-8 运行的标准 Go 库。因此,无法正确解析损坏的 UTF-16 代理项对,例如:
JSON.parse(`"\\uD800"`).charCodeAt(0).toString(16)// 返回 "fffd" 而不是 "d800"
Date
从日历日期(calendar date )到纪元时间戳(epoch timestamp )的转换使用标准 Go 库,该库使用 int,而不是按照 ECMAScript 规范使用 float。 这意味着,如果将溢出 int 的参数传递给 Date() 构造函数,或者存在整数溢出,则结果将不正确,例如:
Date.UTC(1970, 0, 1, 80063993375, 29, 1, -288230376151711740) // returns 29256 instead of 29312
4.本文总结
本文主要和大家介绍 Goja,其是 ECMAScript 5.1 的纯 Go 实现,强调标准合规性和性能,项目很大程度上受到 otto 的启发,而 otto 包是一个用 Go 原生编写的 JavaScript 解析器和解释器。关于 Goja 只是做了一个简短的介绍,但是文末的参考资料提供了大量优秀文档以供学习,如果有兴趣可以自行阅读。如果大家有什么疑问欢迎在评论区留言。
参考资料
https://github.com/dop251/goja
https://pkg.go.dev/github.com/dop251/goja#section-readme
https://github.com/robertkrimen/otto
https://morioh.com/a/08a5a7879bbf/goja-ecmascriptjavascript-engine-in-pure-go
https://www.linkedin.com/pulse/otto-gem-js-library-umakanthan-diwakaran
https://www.linkedin.com/pulse/otto-gem-js-library-umakanthan-diwakaran