由 Babel 理解前端編譯原理

背景

我們知道編程語言主要分爲「編譯型語言」和「解釋型語言」,編譯型語言是在代碼運行前編譯器將編程語言轉換成機器語言,運行時不需要重新翻譯,直接使用編譯的結果就行了。而解釋型語言也是需要將編程語言轉換成機器語言,但是是在運行時轉換的。

通常我們都將 JavaScript 歸類爲「解釋型語言」,以至於很多人都誤以爲前端代碼是不需要編譯的,但其實 JavaScript 引擎進行編譯的步驟和傳統的編譯語言非常相似,只不過與傳統的編譯語言不同,它不是提前編譯的。並且隨着現代瀏覽器和前端領域的蓬勃發展,編譯器在前端領域的應用越來越廣泛,就日常工作而言,包括但不限於以下幾個方面:

作爲前端開發,我們沒必要對這些編譯器或者底層的編譯原理了如指掌,但是如果能對編譯原理有一些基本的認識,也能夠對今後的日常開發很有幫助。本文就帶領大家學習下編譯原理的一些基本概念,並以 Babel 爲例講解下前端編譯的基本流程。

概述

我們先來回顧下編譯原理的基本知識,從宏觀上來說,編譯本質上是一種轉換技術,從一門編程語言轉換成另一門編程語言,或者從高級語言轉換成低級語言,或者從高級語言到高級語言,所謂的高級語言和低級語言主要是指下面的區分:

無論是怎樣的編譯過程,基本都會是下面的一個過程:

上面的約定的編譯規則,就是指各種編程語言的語法規則,不同的編譯器會產出不同的 “編譯結果”,例如 C/C++ 語言經過編譯得到二進制的機器碼,然後交給操作系統,例如當我們運行 tsc 命令就會將 TS 代碼編譯爲 js 代碼,再比如執行 babel 命令會將 es6+ 的代碼編譯爲指定目標 (es5) 的 js 代碼。

一般來說,整個編譯過程主要分爲兩個階段:編譯 前端編譯後端,大致分爲下面的幾個過程:

從上圖可以看到,編譯前端主要就是幫助計算機閱讀源代碼並理解源代碼的結構、含義、作用等,將源代碼由一串無意義的字符流解析爲一個個的有特定含義的構件。通常情況下,編譯前端會產生一種用於給編譯後端消費的中間產物,比如我們常見的抽象語法樹 AST,而編譯後端則是在前端解析的結果和基礎上,進一步優化和轉換並生成最終的目標代碼。

上下文無關文法

前面提到編譯器會根據「約定的編譯規則」進行編譯,這裏「約定的編譯規則」就是指「上下文無關文法(CFG)[20]」。CFG 用於在理論上的形式化定義一門語言的語法,或者說,用於系統地描述程序設計語言的構造(比如表達式和語句)。

實際上,幾乎所有程序設計語言都是通過上下文無關文法來定義的,與正則表達式比較像,但是比正則表達式功能更強大,它能表達非常複雜的文法,比如 C 語言語法用正則表達式來表示不可能做到,但是可以用 CFG 的一組規則來表達。

要理解上下文無關文法,需要先理解下面幾個概念:

例如下面 a, b, c, d 爲終結符(用小寫表示),(S, A) 爲非終結符(用大寫表示)。S -> cAd, A -> a | ab 表示產生式規則。S->cAd,然後可以產生 "cad",“cabd” 等符合文法的內容

S -> cAd
A -> a | ab

上下文無關文法比較抽象,不是這裏學習的重點,感興趣的話可以專門深入瞭解下,這裏知乎上也有篇回答可以參考下 應該如何理解「上下文無關文法」?[21]

下面我們來簡單模擬下如何用 CFG 來定義一門語言的語法, 我們假設一個極其簡單的語言,這個語言只能像 js 那樣聲明整數型常量,以及聲明不接受任何參數且只能直接返回常量加法的箭頭函數。

const a = 10
const b = 20
const c = () =>  a + b

這個語言的文法表達如下:

program :: statement+
statement :: declare | func
declare :: CONST VARIABLE ASSIGN INTEGER
func :: CONST LPAREN RPAREN ARROW expression
expression :: VARIABLE + VARIABLE


CONST  :: "const"
ASSIGN :: "="
LPAREN :: "("
RPAREN :: ")"
ARROW  :: "=>"
INTEGER :: \d+
VARIABLE :: \w[\w\d$_]*

可以看出,整個文法的表達,涵蓋了很多正則表達式的概念。該表達是一種自頂向下的規範:

  1. 首先入口約束了程序(program)是由一條(及以上)的表達式(statement)構成,

  2. 表達式又可以由聲明語句(declare)或函數語句(func)構成。

  3. 聲明語句依次由 const、關鍵字符號 =整數 從左到右排列構成。

  4. 整數的定義則直接使用正則表達式來約束。函數語句也是類似。

大家可以觀察到,上述的文法分成了上下兩個大的部分。上半部分定義了語句以及由語句遞歸構造的表達,通常稱爲「語法規則」(grammar rules);下半部分定義了可通過排列構成語句的基本詞彙,通常稱爲「詞法規則」(lexer rules)。

在實踐中,詞法規則往往沒有單獨羅列,而是直接寫入到語法規則中。比如上述文法可簡化爲:

program :: statement+
statement :: declare | func
declare :: "const" variable "=" integer
func :: "const" variable "=" "(" ")" "=>" expression
expression :: variable + variable
variable :: \w[\w\d$_]*
integer  :: \d+

上面的文法表達形式叫做 BNF[22],它是用來描述上下文無關文法的一種描述語言,形式爲<符號> ::= <使用符號的表達式>,這裏的 <符號> 是非終結符,而表達式由一個符號序列,或用指示選擇的豎槓 '|' 分隔的多個符號序列構成,每個符號序列整體都是左端的符號的一種可能的替代。從未在左端出現的符號就是終結符。

有了 BNF,我們就可以實現語言文法的具體化、公式化,甚至可以自己實現一個語言來解決特定領域的問題。再來看個例子,用 BNF 來描述四則運算:

result ::= number ("+"|"-") exp | number // 非終結符
exp ::= number("*"|"/") exp | number // 非終結符
number ::= [0-9]+ // 終結符

另外,我們也可以窺探下 ECMA 和 JSON 的 BNF:

編譯器工作流程

接下來我們就相對深入的來看一下編譯的各個階段。

詞法分析

就像我們學習一門語言的第一步是學習單詞一樣,編譯器識別源代碼的第一步就是要要進行分詞,識別出每一個單詞或符號。這個階段,詞法分析器會將源代碼拆分成一組 token 串:

  1. 首先,通過對源代碼的字符串從左到右進行掃描,以空白字符(空格、換行、製表符等)爲分隔符,拆分爲一個個無類型的 token 。

  2. 其次,再根據詞法規則,利用「有限狀態機 [25]」對第一步拆分的 Token 進行字符串模式匹配,以識別每一個 Token 的類型(v8 token.h[26])。

一般而言,token 是一個有類型和值的數據結構,而 token 流簡單理解可以是 token 數組。以下面一行代碼爲例:

const name = 'xujianglong';

// 根據 js 的語法規則, 大致會生成如下的 token 流
[
  { type: "CONST", value: "const" },
  { type: "IDENTIFIER", value: "name" },
  { type: "ASSIGN", value: "=" },
  { type: "STRING", value: "xujianglong" },
  { type: "SEMICOLON", value: ";" },
]

那麼有限狀態機是個什麼概念呢?它是怎麼把字符串代碼轉化爲 token 的呢?

首先我們想一下,詞法描述的是最小的單詞格式,比如上面例子的那一行代碼爲例,利用空白字符拆分成這幾個 token:['const','name','=','xujianglong',';'],但是怎麼去識別每種 token 的類型呢,最簡單粗暴的方式我們可以寫個 if else語句或者寫個正則,但是這樣貌似不太優雅且不容易維護,而使用狀態機是許多編程語言都使用的方式。

有限狀態機(英語:finite-state machine,縮寫:FSM)又稱有限狀態自動機(英語:finite-state automation,縮寫:FSA),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行爲的數學計算模型。

如圖所示,用戶從其他狀態機進入 S1 狀態機,如果用戶輸入 1,則繼續進入 S1 狀態機,如果用戶輸入了 0,則進入下一個狀態機 S2。在 S2 狀態機中,如果用戶繼續輸入 1,則繼續進入 S2 狀態機,如果用戶輸入了 0,則回到 S1 狀態機。這是一個循環的過程。

聽起來有點抽象,對比到代碼分詞中來說,我們可以把每個單詞的處理過程當成一種狀態,將整體的輸入(源代碼)按照每個字符依次去讀取,根據每次讀取到的字符來更改當前的狀態,每個 token 識別完了就可以拋出來。我們舉個簡單的四則運算的例子:10 + 20 - 30

首先我們定義了三種狀態機,分別是 NUMBER 代表數值,ADD 代表加號,SUB 代表減號:

  1. 當分析到 "1" 時,因爲本次輸入我們需要改變狀態機內部狀態爲 NUMBER,繼續迭代下一個字符 “0”,此時因爲 "1" 和 "0" 是一個整體可以不被分開的。

  2. 當分析到 "+" 時,狀態機中輸入爲 “+”, 顯然 “+” 是一個運算符號,它並不能和上一次的 “10” 拼接在一起。所以此時狀態改變,我們將上一次的 currentToken 也就是 "10" 推入 tokens 中,同時改變狀態機狀態爲 ADD

  3. 依次類推,最終會輸出如下 tokens 數組:

[
  { type: "NUMBER", value: "10" },
  { type: "ADD", value: "+" },
  { type: "NUMBER", value: "20" },
  { type: "SUB", value: "-" },
  { type: "NUMBER", value: "30" },
]

語法分析

在這個階段,語法分析器(parser)會將詞法分析中得到的 token 數組轉化爲 抽象語法樹 AST 。比如前面定義變量的那行代碼可以在這個在線工具 AST explorer[27] 中查看生成的 AST:

{
  "type""Program",
  "start": 0,
  "end": 28,
  "body"[
    {
      "type""VariableDeclaration",
      "start": 1,
      "end": 28,
      "declarations"[
        {
          "type""VariableDeclarator",
          "start": 7,
          "end": 27,
          "id"{
            "type""Identifier",
            "start": 7,
            "end": 11,
            "name""name"
          },
          "init"{
            "type""Literal",
            "start": 14,
            "end": 27,
            "value""xujianglong",
            "raw""'xujianglong'"
          }
        }
      ],
      "kind""const"
    }
  ],
  "sourceType""module"
}

對於 JavaScript 語言來說,AST 也有一套約定的規範:GitHub - estree/estree: The ESTree Spec[28],社區稱之爲 estree,藉助這個規範,整個前端社區的一些工具便可以產出一套統一的數據格式而無需關心下游,下游的消費類工具統一使用這個統一的格式進行處理而無需關心上游,這樣就做到了上下游的解耦。以 webpack 爲例,其底層是 acorn[29] 工具,acorn 會把 js 源碼轉化爲上述的標準 estree,而 webpack 作爲下游便可以消費該 estree,比如遍歷,提取和分析 require/import 依賴,轉換代碼並輸出。

生成 AST 的過程需要遵循語法規則(用上下文無關文法表示),在上面代碼中,我們用到了「VariableDeclaration」,其語法規則可以表示爲:

VariableDeclaration :: Kind Identifier Init?;
Kind :: Const | Let | Var;
Init :: '=' Expression | Identifier | Literal;
Expression :: BinaryExpression | ConditionalExpression | ...;
Literal :: StringLiteral | ...;

有了語法規則之後,我們就需要思考編譯器是如何將 token 流,在語法規則的約束下,轉換成 AST 的呢?生成 AST 也主要是兩大方向:

  1. 一是將文法規則的約束硬編碼到編譯器的代碼邏輯中,這種是特定語言的編譯器使用的常見方案,這種方案往往是人工編寫 parse 代碼,對輸入源碼的各種錯誤和異常可以更細緻地報告和處理。比如前面提到的 arorn,以及 tsc,babel,以及熟悉的 vue,angular 的模板編譯器等,都主要是這種方法。

  2. 二是使用自動生成工具將文法規則直接轉換成語法 parse 代碼。這種更常用於非特定的編程語言,比如一些業務中自定義的簡單但易變的語法,或僅僅只是字符串文本的複雜處理規則。

我們這裏以第一種方式最基礎的 「遞歸下降算法」(遞歸下降的編譯技術,是業界最常使用的手寫編譯器實現編譯前端的技術之一,感興趣可以專門去深入研究下) 爲例,簡單描述示例代碼生成 AST 的過程:

嘗試匹配 VariableDeclaration

匹配到 Const

匹配到 Identifier

嘗試匹配 Init,遞歸下降

匹配到 '='

嘗試匹配 Expression,遞歸下降

匹配失敗,回溯

匹配到 Literal,回溯

VariableDeclaration 匹配成功,構造相應類型節點插入 AST

語義分析

並不是所有的編譯器都有語義分析,比如 Babel 就沒有。不過對於其它大部分編程語言(包括 TypeScript)的編譯器來說,是有語義分析這一步驟的,特別是靜態類型語言,類型檢查就屬於語義分析的其中一個步驟

語義分析階段,編譯器開始對 AST 進行一次或多次的遍歷,檢查程序的語義規則。主要包括聲明檢查和類型檢查,如上一個賦值語句中,就需要檢查:

語義檢查的步驟和人對源代碼的閱讀和理解的步驟差不多,一般都是在遍歷 AST 的過程中,遇到變量聲明和函數聲明時,則將變量名 -- 類型、函數名 -- 返回類型 -- 參數數量及類型等信息保存到符號表裏,當遇到使用變量和函數的地方,則根據名稱在符號表中查找和檢查,查找該名稱是否被聲明過,該名稱的類型是否被正確的使用等等。

語義檢查時,也會對語法樹進行一些優化,比如將只含常量的表達式先計算出來,如:

a = 1 + 2 * 9;

會被優化成:

a = 19;

語義分析完成後,源代碼的結構解析就已經完成了,所有編譯期錯誤都已被排除,所有使用到的變量名和函數名都綁定到其聲明位置(地址)了,至此編譯器可以說是真正理解了源代碼,可以開始進行代碼生成和代碼優化了。

中間代碼生成與優化

一般的編譯器並不直接生成目標代碼,而是先生成某種中間代碼,然後再生成目標代碼。之所以先生成中間代碼,主要有以下幾個原因:

  1. 爲了降低編譯器開發的難度,將高級語言翻譯成中間代碼、將此中間代碼再翻譯成目標代碼的難度都比直接將高級語言翻譯成目標代碼的難度要低。

  2. 爲了增加編譯器的模塊化、可移植性和可擴展性,一般來說,中間代碼既獨立於任何高級語言,也獨立於任何目標機器架構,這就爲開發出適應性廣泛的編譯器提供了媒介

  3. 爲了代碼優化,一般來說,計算機直接生成的代碼比人手寫的彙編要龐大、重複很多,計算機科學家們對一些具有固定格式的中間代碼的進行大量的研究工作,提出了很多廣泛應用的、效率非常高的優化算法,可以對中間代碼進行優化,比直接對目標代碼進行優化的效果要好很多。

JavaScript 的編譯器 v8 引擎早期是沒有中間代碼生成的,直接從 AST 生成本地可執行的代碼,但由於缺少了轉換爲字節碼這一中間過程,也就減少了優化代碼的機會。爲了提高性能, v8 開始採用了引入字節碼的架構,先把 AST 編譯爲字節碼,再通過 JIT 工具轉換成本地代碼。

v8 引擎的編譯過程這裏不做過多介紹,後續可作爲單獨的分享。

生成目標代碼

有了中間代碼後,目標代碼的生成是相對容易的,因爲中間代碼在設計的時候就考慮到要能輕鬆生成目標代碼。不同的高級語言的編譯器生成的目標代碼不一樣,如:

Babel 的編譯流程

前面我們提到,一般編譯器是指高級語言到低級語言的轉換工具,特殊地像前端的一些工具類型的轉換,如 ts 轉 js,js 轉 js 等這些都是高級語言到高級語言的轉換工具,通常被叫做轉換編譯器,簡稱轉譯器 (Transpiler),所以 Babel 就是是一個 JavaScript 編譯器。

Babel 主要用於將採用 ECMAScript 2015+ 語法編寫的代碼轉換爲向後兼容的 JavaScript 語法,並且還可以把目標環境不支持的 api 進行 polyfill。以便能夠運行在當前和舊版本的瀏覽器或其他環境中。例如下面的例子:

// Babel 輸入:ES2015 箭頭函數
[1, 2, 3].map(n => n + 1);

// Babel 輸出:ES5 語法實現的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});

Babel 的工作流程可分爲如下幾個步驟:

下面詳細介紹下 babel 工作流程的各個階段。

parse(解析)

parse 這個階段將原始代碼字符串轉爲 AST 樹,parse 廣義上來說包括了我們前面編譯流程中講到的 詞法分析、語法分析這兩個階段。parse 過程中會有一些 babel 插件,讓 babel 可以解析出更多的語法,比如 jsx。

parse(sourceCode) => AST

parse 階段,主要通過 @babel/parser這個包進行轉換,之前叫 babylon,是基於 acorn 實現的,擴展了很多語法,可以支持 esnext(現在支持到 es2020)、jsx、flow、typescript 等語法的解析,其中 jsx、flow、typescript 這些非標準的語法的解析需要指定語法插件。

我們可以手動調用下 parser 的方法進行轉換,便會得到一份 AST:

const parser = require("@babel/parser");
const fs = require('fs');

const code = `function square(n) {
  return n * n;};`
const result = parser.parse(code)
console.log(result);

最終輸出的 AST 如下所示:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

可以看到 AST 的每一層都擁有相似的結構,這樣的每一層結構也被叫做   節點(Node), 一個 AST 可以由單一的節點或是成百上千個節點構成,它們組合在一起可以描述用於靜態分析的程序語法。每個節點都有個字符串的 type 類型,用來表示節點的類型,babel 中定義了包含所有 JavaScript 語法的類型,如:

所有的這些節點通過嵌套形成了 AST 樹,例如一個變量賦值語句形成的樹形結構如下所示:

transform(轉化)

transform 階段主要是對上一步 parse 生成的 AST 進行深度優先遍歷,從而對於匹配節點進行增刪改查來修改樹形結構。在 babel 中會用所配置的 plugin 或 presets 對 AST 進行修改後,得到新的 AST,我們的 babel 插件大部分用於這個階段。

transform(AST, BabelPlugins) => newAST

babel 中通過 @babel/traverse 這個包來對 AST 進行遍歷,找出需要修改的節點再進行轉換,這個過程有點類似我們操作 DOM 樹。當我們談及 “進入” 一個節點,實際上是說我們在訪問它們, 之所以使用這樣的術語是因爲有一個訪問者模式(visitor)[32] 的概念。訪問者 visitor 是一個用於 AST 遍歷的跨語言的模式。簡單的說它就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法。

visitor 是一個由各種 type 或者是 enterexit 組成的對象,完成某種類型節點的 "進入" 或 "退出" 兩個步驟則爲一次訪問,在其中可以定義在遍歷 AST 的過程中匹配到某種類型的節點後該如何操作,目前支持的寫法如下:

traverse(ast, {
  /* VisitNodeObject */
  enter(path, state) {},
  exit(path, state) {},
  /* [Type in t.Node["type"]] */
  Identifier(path, state) {}, // 進入 Identifier(標識符節點) 節點時調用
  StringLiteral: { // 進入 StringLiteral(字符串節點) 節點時調用
    enter(path, state) {}, // 進入該節點時的操作
    exit(path, state) {},  // 離開該節點時的操作
  },
  'FunctionDeclaration|VariableDeclaration'(path, state) {}, // // 進入 FunctionDeclaration 和 VariableDeclaration 節點時調用
})

訪問一個節點的過程如下:

下面我們看一個簡單的例子:

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const fs = require('fs');

const code = `function square(n) {
  return n * n;
}`;
const ast = parser.parse(code);
const newAst = traverse(ast, {
  enter(path) {
    if (path.isIdentifier({
        name: "n"
      })) {
      path.node.name = "x";
    }
  },
  FunctionDeclaration: {
    enter() {
      console.log('enter function declaration')
    },
    exit() {
      console.log('exit function declaration')
    }
  }
});

上面的例子中,通過識別標識符把 "n" 換成了 "x",其中的 path 是遍歷過程中的路徑,會保留上下文信息,有很多屬性和方法,可以在訪問到指定節點後,根據 path 進行自定義操作,比如:

有了 @babel/traverse 我們可以在 tranform 階段做很多自定義的事情,例如刪除 console.log 語句,在特定的地方插入一些表達式等等,從而影響輸出結果。我們再舉一個例子,在 console 語句中增加位置信息的輸出,形如:console.log('[18,0]', 111)

import * as t from "@babel/types"; // 用來創建一些 AST 和判斷 AST 的類型

traverse(ast, {
  visitor: {
    CallExpression(path, state) {
      const callee = path.node.callee;
      if (
        callee.object.name === 'console' &&
        ['log''info''error'].includes(callee.property.name)
      ) {
        const { line, column } = path.node.loc.start;
        const locationNode = types.stringLiteral( `[ ${line} , ${column} ]` );
        path .node.arguments.unshift(locationNode);      }
    },
  },
})

generate(生成)

AST 轉換完之後就要輸出目標代碼字符串,這個階段是個逆向操作,用新的 AST 來生成我們所需要的代碼,在生成階段本質上也是遍歷抽象語法樹,根據抽象語法樹上每個節點的類型和屬性遞歸調用從而生成對應的字符串代碼,在 babel 中通過 @babel/generator 包的 api 來實現。

generate(newAST) => newSourceCode

還拿前面的 transform 的代碼舉例,如下所示:

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

const code = `function square(n) {
  return n * n;
}`;

const ast = parser.parse(code);

traverse(ast, {
  enter(path) {
    if (path.isIdentifier({
        name: "n"
      })) {
      path.node.name = "x";
    }
  },
});

const output = generate(ast, {}, code);
console.log(output)

// {
//   code: 'function square(x) { return x * x;}',
//   map: null,
//   rawMappings: undefined
// }

由上面代碼可以看到,函數里的變量 "n" 被替換成了 "x"。

總結

編譯原理涉及大量的概念及知識,以實現完整編譯鏈路的 GCC 編譯器來看,單源代碼就 600w 行,足以說明其水深且寬,但編譯原理帶來的價值是巨大的,可以說是編譯原理的進步纔有各種高級語言百花齊放,進而提高軟件行業生產力。Babel 作爲前端工程化領域一個很重要的工具,明白了其編譯流程,對諸如其他工具 v8 引擎、 tsc、jsx 模板等前端方面的編譯原理便可以融匯貫通,觸類旁通。

參考文檔

以上便是本次分享的全部內容,希望對你有所幫助 ^_^

歡迎關注公衆號  ELab 團隊  收穫大廠一手好文章~

我們來自字節跳動,是旗下大力教育前端部門,負責字節跳動教育全線產品前端開發工作。

參考資料

[1] acorn: https://github.com/acornjs/acorn

[2] 上下文無關文法(CFG): https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E6%97%A0%E5%85%B3%E6%96%87%E6%B3%95

[3] 應該如何理解「上下文無關文法」?: https://www.zhihu.com/question/21833944/answer/307309365

[4] BNF: https://zh.wikipedia.org/wiki/%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F

[5] Syntax - JSON Schema: https://cswr.github.io/JsonSchema/spec/grammar/

[6] function&class bnf: https://tc39.es/ecma262/#sec-ecmascript-language-functions-and-classes

[7] 有限狀態機: https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA

[8] v8 token.h: https://github.com/v8/v8/blob/master/src/parsing/token.h#L56

[9] AST explorer: https://astexplorer.net/

[10] GitHub - estree/estree: The ESTree Spec: https://github.com/estree/estree

[11] acorn: https://github.com/acornjs/acorn

[12] V8 引擎詳解(三)——從字節碼看 V8 的演變: https://juejin.cn/post/6844904152745639949

[13] 理解 V8 的字節碼「譯」: https://zhuanlan.zhihu.com/p/28590489

[14] 訪問者模式(visitor): https://en.wikipedia.org/wiki/Visitor_pattern

[15] Babel 官網: https://babeljs.io/docs/en/

[16] 編譯技術在前端的實踐(一)——編譯原理基礎: https://tech.bytedance.net/articles/7002225912913608735

[17] JavaScript:V8 編譯過程: https://juejin.cn/post/6844903953981767688#heading-0

[18] babel-handbook: https://link.juejin.im/?target=https://github.com/jamiebuilds/babel-handbook

[19] acorn: https://github.com/acornjs/acorn

[20] 上下文無關文法(CFG): https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E6%97%A0%E5%85%B3%E6%96%87%E6%B3%95

[21] 應該如何理解「上下文無關文法」?: https://www.zhihu.com/question/21833944/answer/307309365

[22] BNF: https://zh.wikipedia.org/wiki/%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F

[23] Syntax - JSON Schema: https://cswr.github.io/JsonSchema/spec/grammar/

[24] function&class bnf: https://tc39.es/ecma262/#sec-ecmascript-language-functions-and-classes

[25] 有限狀態機: https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA

[26] v8 token.h: https://github.com/v8/v8/blob/master/src/parsing/token.h#L56

[27] AST explorer: https://astexplorer.net/

[28] GitHub - estree/estree: The ESTree Spec: https://github.com/estree/estree

[29] acorn: https://github.com/acornjs/acorn

[30] V8 引擎詳解(三)——從字節碼看 V8 的演變: https://juejin.cn/post/6844904152745639949

[31] 理解 V8 的字節碼「譯」: https://zhuanlan.zhihu.com/p/28590489

[32] 訪問者模式(visitor): https://en.wikipedia.org/wiki/Visitor_pattern

[33] Babel 官網: https://babeljs.io/docs/en/

[34] JavaScript:V8 編譯過程: https://juejin.cn/post/6844903953981767688#heading-0

[35] babel-handbook: https://link.juejin.im/?target=https://github.com/jamiebuilds/babel-handbook

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Wjw6zdpwcw5LbSmqzkN5RA