首頁 >web前端 >js教程 >用JavaScript寫一個js解釋器

用JavaScript寫一個js解釋器

hzc
hzc轉載
2020-07-02 09:33:023016瀏覽

用js 來編譯js 看起來是個高大上的東西,實際原理其實很簡單,無非就是利用js 物件屬性可以用字串表示 這個特性來實現的黑魔法罷了。
之所以看起來那麼深奧, 大概是由於網上現有的教程,都是動不動就先來個babylon / @babel/parser 先讓大家看個一大串的AST, 然後再貼出一大串的程式碼,
直接遞歸AST 處理所有類型的節點. 最後新手就成功被嚇跑了。

那麼今天我寫這篇的目的,就是給大家一個淺顯易懂,連剛學 js 的人都能看懂的 js2js 教學。

先來看看效果

用JavaScript寫一個js解釋器

一個最簡單的解釋器

上面有提到,js 有個特性是物件屬性可以用字串表示,如console.log 等價於console['log'], 辣麼根據這個特性,我們可以寫出一個兼容性極差,極其簡陋的雛形

  function callFunction(fun, arg) {

    this[fun](arg);

  }

  callFunction('alert', 'hello world');

  // 如果你是在浏览器环境的话,应该会弹出一个弹窗

既然是簡易版的,肯定是問題一大堆,js 裡面得語法不僅僅是函數調用,我們看看賦值是如何用黑魔法實現的

  function declareVarible(key, value) {

    this[key] = value;

  }

  declareVarible.call(window, 'foo', 'bar');

  // window.foo = 'bar'

Tips: const可以利用Object.defineProperty 實作;

如果上面的程式碼能看懂,說明你已經懂得了js 解釋器 的基本原理了,看不懂那隻好怪我咯。

稍微加強一下

可以看出,上面為了方便, 我們把函數呼叫寫成callFunction('alert', 'hello world'); 但是著看起來一點都不像是js 解釋器,
我們心裡想要的解釋器至少應該是長這樣的parse('alert("hello world")''), 那我們來稍微改造一下, 在這裡我們要引入babel 了,
不過先不用擔心, 我們解析出來的語法樹(AST)也是很簡單的。

import babelParser from '@babel/parser';

const code = 'alert("hello world!")';

const ast = babelParser.parse(code);

以上程式碼, 解析出如下內容

{
  "type": "Program",
  "start": 0,
  "end": 21,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 21,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 21,
        "callee": {
          "type": "Identifier",
          "start": 0,
          "end": 5,
          "name": "alert"
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 6,
            "end": 20,
            "value": "hello world!",
            "raw": "\"hello world!\""
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

上面的內容看起來很多,但是我們實際有用到到其實只是很小的一部分, 來稍微簡化一下, 把暫時用不到的欄位先去掉

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "Identifier",
          "name": "alert"
        },
        "arguments": [
          {
            "type": "Literal",
            "value": "hello world!",
          }
        ]
      }
    }
  ],
}

我們大概先瀏覽一次AST 裡面的所有屬性都名為type 的資料

    ##ExpressionStatement
  1. #CallExpression
  2. Identifier
  3. Literal
一共有4 種類型, 那麼接下來我們把這4 種節點分別解析, 從最簡單的開始

Literal

{
    "type": "Literal",
    "value": "hello world!",
}

針對Literal 的內容, 我們需要的只有一個value 屬性, 直接返回即可.

if(node.type === 'Literal') {
    return node.value;
}
是不是很簡單?

Identifier

{
    "type": "Identifier",
    "name": "alert"
},

Identifier 同樣也很簡單, 它代表的就是我們已經存在的一個變數, 變數名稱是node.name, 既然是已經存在的變數, 那麼它的值是什麼呢?

if(node.type === 'Identifier') {
    return {
      name: node.name,
      value:this[node.name]
    };
}
上面的

alert 我們從node.name 裡面拿到的是一個字元, 透過this['xxxxx'] 可以訪問到目前作用域(這裡是window)裡面的這個標識符(Identifier)

ExpressionStatement

{
    "type": "ExpressionStatement",
    "expression": {...}
}

這個其實也是超簡單, 沒有什麼實質性的內容, 真正的內容都在

expression 屬性裡,所以可以直接回傳expression 的內容

if(node.type === 'ExpressionStatement') {
    return parseAstNode(node.expression);
}
CallExpression

CallExpression 按字面的意思理解就是函數呼叫表達式,這個稍微麻煩一點點

{
    "type": "CallExpression",
    "callee": {...},
    "arguments": [...]
}
CallExpression 裡面的有2 個我們需要的欄位:

    callee 是函數的參考, 裡面的內容是一個Identifier, 可以用上面的方法處理.
  1. arguments 裡面的內容是呼叫時傳的參數數組, 我們目前需要處理的是一個Literal, 同樣上面已經有處理方法了.
說到這裡,相信你已經知道怎麼做了

if(node.type === 'CallExpression') {

    // 函数
    const callee = 调用 Identifier 处理器

    // 参数
    const args = node.arguments.map(arg => {
      return 调用 Literal 处理器
    });

    callee(...args);
}
程式碼

這裡有一份簡單的實作, 可以跑通上面的流程, 但也僅僅可以跑通上面而已, 其他的特性都還沒實現。

https://github.com/noahlam/pr...

其他實作方式

除了上面我介紹得這種最繁瑣得方式外,其實js還有好幾種可以直接執行字串程式碼得方式

    插入script DOM
  1.   const script = document.createElement("script");
      script.innerText = 'alert("hello world!")';
      document.body.appendChild(script);
    eval
  1. eval('alert("hello world!")')
    new Function
  1. new Function('alert("hello world")')();
    setTimeout 家族
  1. setTimeout('console.log("hello world")');
不過這些在小程式裡面都被無情得封殺了...

最後,給大家推薦一個
前端學習進階內推交流群685910553(前端資料分享),不管你在地球哪個方位,不管你參加工作幾年都歡迎你的入駐! (群組內會定期免費提供一些群主收藏的免費學習書籍資料以及整理好的面試題和答案文檔!)

如果您對這個文章有任何異議,那麼請在文章評論處寫上你的評論。

如果您覺得這篇文章有意思,那麼請分享並轉發,或者也可以關註一下表示您對我們文章的認可與鼓勵。

願大家都能在程式設計這條路,越走越遠。

推薦教學:《JS教學

以上是用JavaScript寫一個js解釋器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:jianshu.com。如有侵權,請聯絡admin@php.cn刪除