「 giao-js 」用js寫一個js直譯器

null仔發表於2020-11-23

image

前言

在這篇文章中,我們將通過 JS 構建我們自己的 JS 直譯器,用 JS 寫 JS,這聽起來很奇怪,儘管如此,這樣做我們將更熟悉 JS,也可以學習 JS 引擎是如何工作的!

什麼是直譯器 (Interpreter) ?

直譯器是在執行時執行的語言求值器,它動態地執行程式的原始碼。
直譯器解析原始碼,從原始碼生成 AST(抽象語法樹),遍歷 AST 並逐個計算它們。

直譯器 (Interpreter) 工作原理

Interpreter

  • 詞法分析 (Tokenization)
  • 語法解析 (Parsing)
  • 求值 (Evaluating)

詞法分析 (Tokenization)

將原始碼分解並組織成一組有意義的單詞,這一過程即為詞法分析(Token)。

在英語中,當我們遇到這樣一個語句時:

Javascript is the best language in the world

我們會下意識地把句子分解成一個個單詞:

+----------------------------------------------------------+
| Javascript | is | the | best | language | in |the |world |
+----------------------------------------------------------+

這是分析和理解句子的第一階段。

詞法分析是由詞法分析器完成的,詞法分析器會掃描(scanning)程式碼,提取詞法單元。

var a = 1;

[
  ("var": "keyword"),
  ("a": "identifier"),
  ("=": "assignment"),
  ("1": "literal"),
  (";": "separator"),
];

詞法分析器將程式碼分解成 Token 後,會將 Token 傳遞給解析器進行解析,我們來看下解析階段是如何工作的。

語法解析 (Parsing)

將詞法分析階段生成的 Token 轉換為抽象語法樹(Abstract Syntax Tree),這一過程稱之為語法解析(Parsing)。

在英語中,Javascript is the best language 被分解為以下單詞:

+------------------------------------------+
| Javascript | is | the | best | language  |
+------------------------------------------+

這樣我們就可以挑選單詞並形成語法結構:

"Javascript": Subject
"is the best language": Predicate
"language": Object

Javascript 在語法中是一個主語名詞,其餘的是一個沒有什麼意義的句子叫做謂語,language 是動作的接受者,也就是賓語。結構是這樣的:

Subject(Noun) -> Predicate -> Object

語法解析是由語法解析器完成的,它會將上一步生成的 Token,根據語法規則,轉為抽象語法樹(AST)。

{
  type: "Program",
  body: [
    {
      type: "VariableDeclaration",
      declarations: [
        {
          type: "VariableDeclarator",
          id: {
            type: "Identifier",
            name: "sum"
          },
          init: {
            type: "Literal",
            value: 30,
            raw: "30"
          }
        }
      ],
      kind: "var"
    }
  ],
}

求值階段 (Evaluating)

直譯器將遍歷 AST 並計算每個節點。- 求值階段
1 + 2
|
    |
    v
+---+  +---+
| 1 |  | 2 |
+---+  +---+
  \     /
   \   /
    \ /
   +---+
   | + |
   +---+
{
    lhs: 1,
    op: '+'.
    rhs: 2
}

直譯器解析 Ast,得到 LHS 節點,接著收集到操作符(operator)節點+,+操作符表示需要進行一次加法操作,它必須有第二個節點來進行加法操作.接著他收集到 RHS 節點。它收集到了有價值的資訊並執行加法得到了結果,3。

{
  type: "Program",
  body: [
    {
      type: "ExpressionStatement",
      expression: {
        type: "BinaryExpression",
        left: {
          type: "Literal",
          value: 1,
          raw: "1"
        },
        operator: "+",
        right: {
          type: "Literal",
          value: 2,
          raw: "2"
        }
      }
    }
  ],
}

實踐

前面我們已經介紹瞭直譯器的工作原理,接下來我們來動動手鬆鬆筋骨吧,實現一個 Mini Js Interpreter~

實踐準備

  • Acorn.js
A tiny, fast JavaScript parser, written completely in JavaScript. 一個完全使用 javascript 實現的,小型且快速的 javascript 解析器

本次實踐我們將使用 acorn.js ,它會幫我們進行詞法分析,語法解析並轉換為抽象語法樹。

Webpack/Rollup/Babel(@babel/parser) 等第三方庫也是使用 acorn.js 作為自己 Parser 的基礎庫。(站在巨人的肩膀上啊!)

  • The Estree Spec

最開始 Mozilla JS Parser API 是 Mozilla 工程師在 Firefox 中建立的 SpiderMonkey 引擎輸出 JavaScript AST 的規範文件,文件所描述的格式被用作操作 JAvaScript 原始碼的通用語言。

隨著 JavaScript 的發展,更多新的語法被加入,為了幫助發展這種格式以跟上 JavaScript 語言的發展。The ESTree Spec 就誕生了,作為參與構建和使用這些工具的人員的社群標準。

acorn.js parse 返回值符合 ESTree spec 描述的 AST 物件,這裡我們使用@types/estree 做型別定義。

  • Jest

號稱令人愉快的 JavaScript 測試...我們使用它來進行單元測試.

  • Rollup

Rollup 是一個 JavaScript 模組打包器,我們使用它來打包,以 UMD 規範對外暴露模組。

專案初始化

// visitor.ts 建立一個Visitor類,並提供一個方法操作ES節點。
import * as ESTree from "estree";
class Visitor {
  visitNode(node: ESTree.Node) {
    // ...
  }
}
export default Visitor;
// interpreter.ts 建立一個Interpreter類,用於執行ES節點樹。
// 建立一個Visitor例項,並使用該例項來執行ESTree節點
import Visitor from "./visitor";
import * as ESTree from "estree";
class Interpreter {
  private visitor: Visitor;
  constructor(visitor: Visitor) {
    this.visitor = visitor;
  }
  interpret(node: ESTree.Node) {
    this.visitor.visitNode(node);
  }
}
export default Interpreter;
// vm.ts 對外暴露run方法,並使用acorn code->ast後,交給Interpreter例項進行解釋。
const acorn = require("acorn");
import Visitor from "./visitor";
import Interpreter from "./interpreter";

const jsInterpreter = new Interpreter(new Visitor());

export function run(code: string) {
  const root = acorn.parse(code, {
    ecmaVersion: 8,
    sourceType: "script",
  });
  return jsInterpreter.interpret(root);
}

實踐第 1 彈: 1+1= ?

我們這節來實現 1+1 加法的解釋。首先我們通過AST explorer,看看 1+1 這段程式碼轉換後的 AST 結構。

1+1 ast

我們可以看到這段程式碼中存在 4 種節點型別,下面我們簡單的介紹一下它們:

Program

根節點,即代表一整顆抽象語法樹,body 屬性是一個陣列,包含了多個 Statement 節點。

interface Program {
  type: "Program";
  sourceType: "script" | "module";
  body: Array<Directive | Statement | ModuleDeclaration>;
  comments?: Array<Comment>;
}

ExpressionStatement

表示式語句節點,expression 屬性指向一個表示式節點物件

interface ExpressionStatement {
  type: "ExpressionStatement";
  expression: Expression;
}

BinaryExpression

二元運算表示式節點,left 和 right 表示運算子左右的兩個表示式,operator 表示一個二元運算子。
本節實現的重點,簡單理解,我們只要拿到 operator 操作符的型別並實現,然後對 left,right 值進行求值即可。

interface BinaryExpression {
  type: "BinaryExpression";
  operator: BinaryOperator;
  left: Expression;
  right: Expression;
}

Literal

字面量,這裡不是指 [] 或者 {} 這些,而是本身語義就代表了一個值的字面量,如 1,“hello”, true 這些,還有正規表示式,如 /\d?/。

type Literal = SimpleLiteral | RegExpLiteral;

interface SimpleLiteral {
  type: "Literal";
  value: string | boolean | number | null;
  raw?: string;
}

interface RegExpLiteral {
  type: "Literal";
  value?: RegExp | null;
  regex: {
    pattern: string;
    flags: string;
  };
  raw?: string;
}

廢話少說,開擼!!!

// standard/es5.ts 實現以上節點方法

import Scope from "../scope";
import * as ESTree from "estree";
import { AstPath } from "../types/index";

const es5 = {
  // 根節點的處理很簡單,我們只要對它的body屬性進行遍歷,然後訪問該節點即可。
  Program(node: ESTree.Program) {
    node.body.forEach((bodyNode) => this.visitNode(bodyNode));
  },
  // 表示式語句節點的處理,同樣訪問expression 屬性即可。
  ExpressionStatement(node: ESTree.ExpressionStatement>) {
    return this.visitNode(node.expression);
  },
  // 字面量節點處理直接求值,這裡對正規表示式型別進行了特殊處理,其他型別直接返回value值即可。
  Literal(node: ESTree.Literal>) {
    if ((<ESTree.RegExpLiteral>node).regex) {
      const { pattern, flags } = (<ESTree.RegExpLiteral>node).regex;
      return new RegExp(pattern, flags);
    } else return node.value;
  },
  // 二元運算表示式節點處理
  // 對left/node兩個節點(Literal)進行求值,然後實現operator型別運算,返回結果。
  BinaryExpression(node: ESTree.BinaryExpression>) {
    const leftNode = this.visitNode(node.left);
    const operator = node.operator;
    const rightNode = this.visitNode(node.right);
    return {
      "+": (l, r) => l + r,
      "-": (l, r) => l - r,
      "*": (l, r) => l * r,
      "/": (l, r) => l / r,
      "%": (l, r) => l % r,
      "<": (l, r) => l < r,
      ">": (l, r) => l > r,
      "<=": (l, r) => l <= r,
      ">=": (l, r) => l >= r,
      "==": (l, r) => l == r,
      "===": (l, r) => l === r,
      "!=": (l, r) => l != r,
      "!==": (l, r) => l !== r,
    }[operator](leftNode, rightNode);
  },
};
export default es5;
// visitor.ts
import Scope from "./scope";
import * as ESTree from "estree";
import es5 from "./standard/es5";

const VISITOR = {
  ...es5,
};
class Visitor {
  // 實現訪問節點方法,通過節點型別訪問對應的節點方法
  visitNode(node: ESTree.Node) {
    return {
      visitNode: this.visitNode,
      ...VISITOR,
    }[node.type](node);
  }
}
export default Visitor;

就這樣,普通的二元運算就搞定啦!!!

實踐第 2 彈: 怎麼找到變數?

Javascript 的作用域與作用域鏈的概念想必大家都很熟悉了,這裡就不再囉嗦了~

是的,我們需要通過實現作用域來訪問變數,實現作用域鏈來搜尋識別符號。

在這之前,我們先實現 Variable 類,實現變數的存取方法。

// variable.ts
export enum Kind {
  var = "var",
  let = "let",
  const = "const",
}
export type KindType = "var" | "let" | "const";
export class Variable {
  private _value: any;
  constructor(public kind: Kind, val: any) {
    this._value = val;
  }
  get value() {
    return this._value;
  }
  set value(val: any) {
    this._value = val;
  }
}
import { Variable, Kind, KindType } from "./variable";

class Scope {
  // 父作用域
  private parent: Scope | null;
  // 當前作用域
  private targetScope: { [key: string]: any };
  constructor(public readonly type, parent?: Scope) {
    this.parent = parent || null;
    this.targetScope = new Map();
  }
  // 是否已定義
  private hasDefinition(rawName: string): boolean {
    return Boolean(this.search(rawName));
  }
  // var型別變數定義
  public defineVar(rawName: string, value: any) {
    let scope: Scope = this;
    // 如果不是全域性作用域且不是函式作用域,找到全域性作用域,儲存變數
    // 這裡就是我們常說的Hoisting (變數提升)
    while (scope.parent && scope.type !== "function") {
      scope = scope.parent;
    }
    // 儲存變數
    scope.targetScope.set(rawName, new Variable(Kind.var, value));
  }
  // let型別變數定義
  public defineLet(rawName: string, value: any) {
    this.targetScope.set(rawName, new Variable(Kind.let, value));
  }
  // const型別變數定義
  public defineConst(rawName: string, value: any) {
    this.targetScope.set(rawName, new Variable(Kind.const, value));
  }
  // 作用域鏈實現,向上查詢識別符號
  public search(rawName: string): Variable | null {
    if (this.targetScope.get(rawName)) {
      return this.targetScope.get(rawName);
    } else if (this.parent) {
      return this.parent.search(rawName);
    } else {
      return null;
    }
  }
  // 變數宣告方法,變數已定義則丟擲語法錯誤異常
  public declare(kind: Kind | KindType, rawName: string, value: any) {
    if (this.hasDefinition(rawName)) {
      console.error(
        `Uncaught SyntaxError: Identifier '${rawName}' has already been declared`
      );
      return true;
    }
    return {
      [Kind.var]: () => this.defineVar(rawName, value),
      [Kind.let]: () => this.defineLet(rawName, value),
      [Kind.const]: () => this.defineConst(rawName, value),
    }[kind]();
  }
}

export default Scope;

以上就是變數物件,作用域及作用域鏈的基礎實現了,接下來我們就可以定義及訪問變數了。

實踐第 3 彈: var age = 18

var

從語法樹中我們可以看到三個陌生的節點型別,來看看它們分別代表什麼意思:

VariableDeclaration

變數宣告,kind 屬性表示是什麼型別的宣告,因為 ES6 引入了 const/let。
declarations 表示宣告的多個描述,因為我們可以這樣:let a = 1, b = 2;。

interface VariableDeclaration {
  type: "VariableDeclaration";
  declarations: Array<VariableDeclarator>;
  kind: "var" | "let" | "const";
}

VariableDeclarator

變數宣告的描述,id 表示變數名稱節點,init 表示初始值的表示式,可以為 null。

interface VariableDeclarator {
  type: "VariableDeclarator";
  id: Pattern;
  init?: Expression | null;
}

Identifier

顧名思義,識別符號節點,我們寫 JS 時定義的變數名,函式名,屬性名,都歸為識別符號。

interface Identifier {
  type: "Identifier";
  name: string;
}

瞭解了對應節點的含義後,我們來進行實現:

// standard/es5.ts 實現以上節點方法

import Scope from "../scope";
import * as ESTree from "estree";

type AstPath<T> = {
  node: T;
  scope: Scope;
};

const es5 = {
  // ...
  // 這裡我們定義了astPath,新增了scope作用域引數
  VariableDeclaration(astPath: AstPath<ESTree.VariableDeclaration>) {
    const { node, scope } = astPath;
    const { declarations, kind } = node;
    // 上面提到,生宣告可能存在多個描述(let a = 1, b = 2;),所以我們這裡對它進行遍歷:
    // 這裡遍歷出來的每個item是VariableDeclarator節點
    declarations.forEach((declar) => {
      const { id, init } = <ESTree.VariableDeclarator>declar;
      // 變數名稱節點,這裡拿到的是age
      const key = (<ESTree.Identifier>id).name;
      // 判斷變數是否進行了初始化 ? 查詢init節點值(Literal型別直接返回值:18) : 置為undefined;
      const value = init ? this.visitNode(init, scope) : undefined;
      // 根據不同的kind(var/const/let)宣告進行定義,即var age = 18
      scope.declare(kind, key, value);
    });
  },
  // 識別符號節點,我們只要通過訪問作用域,訪問該值即可。
  Identifier(astPath: AstPath<ESTree.Identifier>) {
    const { node, scope } = astPath;
    const name = node.name;
    // walk identifier
    // 這個例子中查詢的是age變數
    const variable = scope.search(name);
    // 返回的是定義的變數物件(age)的值,即18
    if (variable) return variable.value;
  },
};
export default es5;

實踐第 4 彈: module.exports = 6

我們先來看看 module.exports = 6 對應的 AST。

module-exports

從語法樹中我們又看到兩個陌生的節點型別,來看看它們分別代表什麼意思:

AssignmentExpression

賦值表示式節點,operator 屬性表示一個賦值運算子,left 和 right 是賦值運算子左右的表示式。

interface AssignmentExpression {
  type: "AssignmentExpression";
  operator: AssignmentOperator;
  left: Pattern | MemberExpression;
  right: Expression;
}

MemberExpression

成員表示式節點,即表示引用物件成員的語句,object 是引用物件的表示式節點,property 是表示屬性名稱,computed 如果為 false,是表示 . 來引用成員,property 應該為一個 Identifier 節點,如果 computed 屬性為 true,則是 [] 來進行引用,即 property 是一個 Expression 節點,名稱是表示式的結果值。

interface MemberExpression {
  type: "MemberExpression";
  object: Expression | Super;
  property: Expression;
  computed: boolean;
  optional: boolean;
}

我們先來定義 module.exports 變數。

import Scope from "./scope";
import Visitor from "./visitor";
import * as ESTree from "estree";
class Interpreter {
  private scope: Scope;
  private visitor: Visitor;
  constructor(visitor: Visitor) {
    this.visitor = visitor;
  }
  interpret(node: ESTree.Node) {
    this.createScope();
    this.visitor.visitNode(node, this.scope);
    return this.exportResult();
  }
  createScope() {
    // 建立全域性作用域
    this.scope = new Scope("root");
    // 定義module.exports
    const $exports = {};
    const $module = { exports: $exports };
    this.scope.defineConst("module", $module);
    this.scope.defineVar("exports", $exports);
  }
  // 模擬commonjs,對外暴露結果
  exportResult() {
    // 查詢module變數
    const moduleExport = this.scope.search("module");
    // 返回module.exports值
    return moduleExport ? moduleExport.value.exports : null;
  }
}
export default Interpreter;

ok,下面我們來實現以上節點函式~

// standard/es5.ts 實現以上節點方法

import Scope from "../scope";
import * as ESTree from "estree";

type AstPath<T> = {
  node: T;
  scope: Scope;
};

const es5 = {
  // ...
  // 這裡我們定義了astPath,新增了scope作用域引數
  MemberExpression(astPath: AstPath<ESTree.MemberExpression>) {
    const { node, scope } = astPath;
    const { object, property, computed } = node;
    // property 是表示屬性名稱,computed 如果為 false,property 應該為一個 Identifier 節點,如果 computed 屬性為 true,即 property 是一個 Expression 節點
    // 這裡我們拿到的是exports這個key值,即屬性名稱
    const prop = computed
      ? this.visitNode(property, scope)
      : (<ESTree.Identifier>property).name;
    // object 表示物件,這裡為module,對module進行節點訪問
    const obj = this.visitNode(object, scope);
    // 訪問module.exports值
    return obj[prop];
  },
  // 賦值表示式節點
  (astPath: AstPath<ESTree.>) {
    const { node, scope } = astPath;
    const { left, operator, right } = node;
    let assignVar;
    // LHS 處理
    if (left.type === "Identifier") {
      // 識別符號型別 直接查詢
      const value = scope.search(left.name);
      assignVar = value;
    } else if (left.type === "MemberExpression") {
      // 成員表示式型別,處理方式跟上面差不多,不同的是這邊需要自定義一個變數物件的實現
      const { object, property, computed } = left;
      const obj = this.visitNode(object, scope);
      const key = computed
        ? this.visitNode(property, scope)
        : (<ESTree.Identifier>property).name;
      assignVar = {
        get value() {
          return obj[key];
        },
        set value(v) {
          obj[key] = v;
        },
      };
    }
    // RHS
    // 不同操作符處理,查詢到right節點值,對left節點進行賦值。
    return {
      "=": (v) => {
        assignVar.value = v;
        return v;
      },
      "+=": (v) => {
        const value = assignVar.value;
        assignVar.value = v + value;
        return assignVar.value;
      },
      "-=": (v) => {
        const value = assignVar.value;
        assignVar.value = value - v;
        return assignVar.value;
      },
      "*=": (v) => {
        const value = assignVar.value;
        assignVar.value = v * value;
        return assignVar.value;
      },
      "/=": (v) => {
        const value = assignVar.value;
        assignVar.value = value / v;
        return assignVar.value;
      },
      "%=": (v) => {
        const value = assignVar.value;
        assignVar.value = value % v;
        return assignVar.value;
      },
    }[operator](this.visitNode(right, scope));
  },
};
export default es5;

ok,實現完畢,是時候驗證一波了,上 jest 大法。

// __test__/es5.test.ts

import { run } from "../src/vm";
describe("giao-js es5", () => {
  test("assign", () => {
    expect(
      run(`
      module.exports = 6;
    `)
    ).toBe(6);
  });
}

jest

實踐第 5 彈: for 迴圈

var result = 0;
for (var i = 0; i < 5; i++) {
  result += 2;
}
module.exports = result;

for-loop

到這一彈大家都發現了,不同的語法其實對應的就是不同的樹節點,我們只要實現對應的節點函式即可.我們先來看看這幾個陌生節點的含義.

ForStatement

for 迴圈語句節點,屬性 init/test/update 分別表示了 for 語句括號中的三個表示式,初始化值,迴圈判斷條件,每次迴圈執行的變數更新語句(init 可以是變數宣告或者表示式)。
這三個屬性都可以為 null,即 for(;;){}。
body 屬性用以表示要迴圈執行的語句。

interface ForStatement {
  type: "ForStatement";
  init?: VariableDeclaration | Expression | null;
  test?: Expression | null;
  update?: Expression | null;
  body: Statement;
}

UpdateExpression

update 運算表示式節點,即 ++/--,和一元運算子類似,只是 operator 指向的節點物件型別不同,這裡是 update 運算子。

interface UpdateExpression {
  type: "UpdateExpression";
  operator: UpdateOperator;
  argument: Expression;
  prefix: boolean;
}

BlockStatement

塊語句節點,舉個例子:if (...) { // 這裡是塊語句的內容 },塊裡邊可以包含多個其他的語句,所以有一個 body 屬性,是一個陣列,表示了塊裡邊的多個語句。

interface BlockStatement {
  0;
  type: "BlockStatement";
  body: Array<Statement>;
  innerComments?: Array<Comment>;
}

廢話少說,盤它!!!

// standard/es5.ts 實現以上節點方法

import Scope from "../scope";
import * as ESTree from "estree";

type AstPath<T> = {
  node: T;
  scope: Scope;
};

const es5 = {
  // ...
  // for 迴圈語句節點
  ForStatement(astPath: AstPath<ESTree.ForStatement>) {
    const { node, scope } = astPath;
    const { init, test, update, body } = node;
    // 這裡需要注意的是需要模擬建立一個塊級作用域
    // 前面Scope類實現,var宣告在塊作用域中會被提升,const/let不會
    const forScope = new Scope("block", scope);
    for (
      // 初始化值
      // VariableDeclaration
      init ? this.visitNode(init, forScope) : null;
      // 迴圈判斷條件(BinaryExpression)
      // 二元運算表示式,之前已實現,這裡不再細說
      test ? this.visitNode(test, forScope) : true;
      // 變數更新語句(UpdateExpression)
      update ? this.visitNode(update, forScope) : null
    ) {
      // BlockStatement
      this.visitNode(body, forScope);
    }
  },
  // update 運算表示式節點
  // update 運算表示式節點,即 ++/--,和一元運算子類似,只是 operator 指向的節點物件型別不同,這裡是 update 運算子。
  UpdateExpression(astPath: AstPath<ESTree.UpdateExpression>) {
    const { node, scope } = astPath;
    // update 運算子,值為 ++ 或 --,配合 update 表示式節點的 prefix 屬性來表示前後。
    const { prefix, argument, operator } = node;
    let updateVar;
    // 這裡需要考慮引數型別還有一種情況是成員表示式節點
    // 例: for (var query={count:0}; query.count < 8; query.count++)
    // LHS查詢
    if (argument.type === "Identifier") {
      // 識別符號型別 直接查詢
      const value = scope.search(argument.name);
      updateVar = value;
    } else if (argument.type === "MemberExpression") {
      // 成員表示式的實現在前面實現過,這裡不再細說,一樣的套路~
      const { object, property, computed } = argument;
      const obj = this.visitNode(object, scope);
      const key = computed
        ? this.visitNode(property, scope)
        : (<ESTree.Identifier>property).name;
      updateVar = {
        get value() {
          return obj[key];
        },
        set value(v) {
          obj[key] = v;
        },
      };
    }
    return {
      "++": (v) => {
        const result = v.value;
        v.value = result + 1;
        // preifx? ++i: i++;
        return prefix ? v.value : result;
      },
      "--": (v) => {
        const result = v.value;
        v.value = result - 1;
        // preifx? --i: i--;
        return prefix ? v.value : result;
      },
    }[operator](updateVar);
  },
  // 塊語句節點
  // 塊語句的實現很簡單,模擬建立一個塊作用域,然後遍歷body屬性進行訪問即可。
  BlockStatement(astPath: AstPath<ESTree.BlockStatement>) {
    const { node, scope } = astPath;
    const blockScope = new Scope("block", scope);
    const { body } = node;
    body.forEach((bodyNode) => {
      this.visitNode(bodyNode, blockScope);
    });
  },
};
export default es5;

上 jest 大法驗證一哈~

test("test for loop", () => {
  expect(
    run(`
      var result = 0;
      for (var i = 0; i < 5; i++) {
        result += 2;
      }
      module.exports = result;
    `)
  ).toBe(10);
});

for-loop-jest

你以為這樣就結束了嗎? 有沒有想到還有什麼情況沒處理? for 迴圈的中斷語句呢?

var result = 0;
for (var i = 0; i < 5; i++) {
  result += 2;
  break; // break,continue,return
}
module.exports = result;

感興趣的小夥伴可以自己動手試試,或者戳原始碼地址

結語

giao-js目前只實現了幾個語法,本文只是提供一個思路。

有興趣的同學可以檢視完整程式碼

覺得有幫助到你的話,點個 star 支援下作者 ❤️ ~

參考

bramblex/jsjs

使用 Acorn 來解析 JavaScript

Build a JS Interpreter in JavaScript Using Acorn as a Parser

相關文章