go實現一個js解釋器

豆皮粉兒們,你們好呀。愉快的五一節就這麼過去了,假期有沒有好好遊玩一番呢。今天由清風慕竹給你們帶來一篇《如何用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

image-20210125074255009

其中轉換步驟是可選的,這一步主要工做是將語法樹上的節點轉換成目標語言可執行的節點,對於eval5這種js-in-js的實現,這一步就不須要實現了。可是對js-in-x(x多是go、c++、py)這種狀況則須要增長轉換的步驟。關於詞法分析、語法解析這兩塊實現資料比較多,這裏再也不贅述,熟悉js的同窗能夠參考acornbabel-parserespree等實現,這裏重點講下轉換和遍歷執行的過程。golang

go與js數據交換

在轉換、執行以前須要先解決go和js數據交換的問題,須要考慮 js< --- >go 雙向的場景。express

  1. go代碼訪問js變量

js代碼在ast語法樹轉換的過程當中,對應的ast節點轉換的過程當中被轉換成expression節點,基本的值被裝箱成Value類型,golang訪問js變量實際上訪問的是變量對應的ast節點轉換後生成的expression節點。 好比在js中定義以下:json

var a = 2;
function print(name) {
  console.log('hello ' + name)
}
複製代碼

變量定義轉換以下:數組

image-20210125171519499

函數定義轉換以下:安全

image-20210125170736066

golang在執行前會處理變量定義,處理以後會在對應做用域對象上生成key(變量名or函數名)到expression的binding:微信

image-20210125175722806

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上了:

image-20210125181404804

執行ast轉換後的節點時發現須要獲取identifier x對應值的時候,會從property對應的map上拿到x對應的值。函數也是如此。

轉換

  • 從ast樹的body節點開始遍歷,依次執行statement轉換的過程

    好比上圖中 1+1在ast樹上對應的節點是ExpressionStatement,對應的會依次調用parseStatement:

    image-20210125083936749

    parseExpression:

    image-20210125084026214

    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數組中。 image-20210125084629642

遍歷、執行以

1+1 爲例:

image-20210125183015895

遍歷ast執行對應的節點時須要注意,js存在做用域的區別(全局做用域、函數做用域)。在上面的代碼執行的時候,默認將代碼放在全局做用域執行:

image-20210125153241238

enterGlobalScope和leaveScope對應實現以下:

image-20210125153636497

進入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節點,並根據節點類型執行對應的操做:

image-20210125160131093

接着執行ExpressionStatement中的expression,對應類型是BinaryExpression:

image-20210125161116991

BinaryExpression須要計算左值和右值,先計算node.left:

image-20210125161156947

再進入cmp_evaluate_nodeExpression時,此時expression類型爲nodeLiteral, 直接返回node.value便可。下面根據node.operator執行對應的計算邏輯(參與的時候先對Value類型執行拆箱,獲取基本類型的值後轉換成float64類型參與計算,計算的結果以Value的包裝類型返回):

image-20210125161807232

到這裏就完成了1+1的計算。

總結

藉助現有的js代碼解析庫能夠相對容易的實現一個js的解釋器,實現思路比較明確,可是對於新語法規範支持度仍是比較差,後面能夠進一步擴充語法。除了本文介紹的js-in-go的實現以外,js-in-js也有一些比較有意思的玩法,好比藉助js-in-js的實現,能夠在js引擎屏蔽了eval、new Function狀況下實現js代碼的熱更新。

更多精彩內容,定製禮品圖書贈送,高薪職位內推,微信搜索關注「豆皮範兒」

相關文章
相關標籤/搜索