[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

Colafornia發表於2018-01-12

[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

概述

在之前的文章中,我們從事件執行機制詳細地講解了 JavaScript 引擎是如何工作的,同時也簡略地提到了編譯的知識。是的,你沒看錯。JavaScript 是編譯的,儘管它並不像其它語言編譯器有可以進行提前優化的構建階段,JavaScript 不得不在最後一秒編譯程式碼 —— 從字面上看。用於編譯 JavaScript 的技術有一個十分恰當的名字,即時編譯器(JIT)。這種 "即時編譯" 技術已經應用到現代 JavaScript 引擎中,用於實現瀏覽器的加速。

開發者將 JavaScript 稱為解釋型語言,這會讓人有點困惑。因為直到最近,JavaScript 引擎總是和直譯器聯絡在一起。現在,伴隨著像 Google V8 這樣的引擎出現,開發者們實現了魚與熊掌兼得 —— 既擁有直譯器也擁有編譯器的引擎。

下面我們將展示這些流行的 JIT 編譯器是怎麼處理 JavaScript 程式碼的。引擎優化程式碼的複雜機制(如內聯(去除空格),利用隱藏類以及消除冗餘程式碼等)不在本文的討論範圍內。與之相反,本文著眼於編譯原理,讓你瞭解現代的 JavaScript 引擎內部是如何工作的。

免責宣告: 看完這篇文章你可能會變成程式碼素食主義者。

語言與程式碼

[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

為了能夠 心意相通 地領會編譯器是怎麼讀懂程式碼的,你可以先想一下你此刻讀文章時使用的語言:英語。我們都在開發控制檯裡看到過鮮紅的 SyntaxError 報錯,當我們抓破腦袋去找是哪裡少了一個分號時,也許都想起過 Noam Chomsky。他將語法定義為:

“研究以特定語言構造句子的原則和過程。”

我們在 Noam Chomsky 的定義的基礎上呼叫 “內建” 的 simplify() 函式。

simplify(quote, "grossly")

// 結果:語言的順序並不相同

當然,Chomsky 的定義是指德語和斯瓦西里等語言,而不是 JavaScript 和 Ruby。儘管如此,高階程式語言脫離了我們所說的語言。實質上,JavaScript 編譯器已經被精明的工程師們 “教會” 閱讀 JavaScript 程式碼,像我們的父母老師訓練我們讀懂句子一樣。

我們可以觀察出,語言學中的三個方面都與編譯器有關:詞法單元,語法和語義。換句話說,也就是研究單詞的含義及其關係,研究單詞的排列以及研究句子的含義(為了適應我們的場景,在此處限制了語義的定義)。

以這個句子為例: We ate beef.

詞法單元

請注意句子裡的每個單詞是如何被分解成具有詞彙含義的單位:We/ate/beef

語法

這個基礎的句子在語法上遵循了主語 / 動詞 / 賓語的協議。假設這就是每個英文句子必須遵從的構造方式。為什麼要做這樣的假設?因為編譯器必須在嚴格的規定下工作,這樣才能檢測到語法錯誤。因此,Beef we ate, 雖然仍是一個可以理解的句子,但在我們假設出的極簡版英文語法規定中會是錯誤的。

語義

從語義上講,每個句子都有它的含義。我們知道許許多多的人過去都吃過牛肉。我們就可以通過把句子改寫成 We+ beef ate 來剝離出它的語義。


現在,我們英文中原有的 句子 翻譯成 JavaScript 表示式

let sentence = “We ate beef”;

詞法單元

表示式可以被分解成詞素: let/sentence/=/ “We ate beef”/;

語法

我們的表示式,像句子一樣必須是遵從語法構造的。JavaScript 以及大多數其它程式語言都遵從 (型別) / 變數 / 賦值 / 值 的順序。型別是適應於上下文的。如果你也困擾於寬鬆的型別宣告,可以給程式的全域性作用域加上 “use strict”;“use strict”; 是一種可以強制執行 JavaScript 語法規則的嚴格語法。相信我,使用 “use strict”; 利遠大於弊。

語義

從語義上講,我們的程式碼都具有最終能被機器通過編譯器來理解的含義。為了取到程式碼中的語義,編譯器必須去讀程式碼。我們在下一節深入研究這一環節。

提示: 上下文與作用域是不一樣的。做更深層的闡述的話就超出了本文的 “作用域”。

LHS/RHS

我們讀英文是按照從左往右的順序,編譯器讀程式碼卻是雙向的。編譯器是怎麼做到的?通過 LHS 查詢 和 RHS 查詢。我們來深入看看它們是怎麼一回事。

LHS 查詢聚焦於賦值操作的 “左邊”。意思就是 LHS 負責查詢賦值操作的 目標。我們要使用 目標 這個概念而不是 位置,因為 LHS 查詢的目標可能位置不同。並且,賦值操作 也並不一定顯式地指向 賦值運算子

為了解釋地更清楚,我們來看看下面這個例子:

function square(a){
    return a*a;

}

square(5);
複製程式碼

這個函式會調起一次針對 a 的 LHS 查詢。為什麼?因為我們把 5 作為引數傳入這個函式,並隱式地將它的值賦給了 a。注意,不可能一眼就看出賦值目標是什麼,必須通過推斷得出。

相反地,RHS 查詢聚焦於值本身。回顧剛才的例子,RHS 查詢會在 a*a; 表示式裡找到 a 的值。

還有很重要的一點,這些查詢操作是出現在編譯的最後階段,程式碼生成階段。等講到那一步我們將進一步闡述。現在我們來探索一下編譯器。

編譯器

把編譯器想象成一個肉製品加工廠,有幾種機制把程式碼研磨成計算機認為可食用或可執行的包。在這個例子中,我們將處理表示式。

[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

標記解析器

首先,標記解析器將程式碼分解成稱為 token 的單元。

[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

這些 token 隨後會被標記解析器標記。當標記解析器發現一個不屬於該語言的 “字母” 時,會出現詞法錯誤。請記住,這和語法錯誤不一樣。例如,如果我們使用了 @ 符號而不是賦值運算子,那麼標記解析器就會看到 @ 符號,並且說:“嗯......這個詞法在 JavaScript 的詞典裡找不到......紅色警戒,關掉所有東西

提示: 如果這個系統能夠在一個標記和另一個標記之間進行關聯,然後像解析器一樣將它們組合在一起,那麼它將被視為一個詞法分析器

[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

語法分析器

語法分析器會去查詢語法錯誤。如果沒有錯誤的話,語法分析器會把 token 打包成被一種被稱為解析語法樹的結構。在編譯的這一環節,JavaScript 程式碼被視為已解析過,將要進行語義分析的。再一次,如果遵循了 JavaScript 規則,則會產生一個被稱為抽象語法樹 (AST) 的資料結構。

[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

這就是簡化版的 AST


還有一個 中間步驟 ,直譯器將原始碼按照宣告語句,逐個轉換為中間程式碼(通常為位元組碼)。位元組碼隨後在虛擬機器內執行。

然後,程式碼會被優化,這其中包含了移除空格,不會被執行的死碼和冗餘程式碼,以及其它很多優化過程。


程式碼生成器

一旦程式碼優化完畢,程式碼生成器的工作是將中間程式碼轉換為機器可以理解的底層組合語言。此時,生成器負責:

(1) 確保底層程式碼保留與原始碼相同的指令

(2) 將位元組碼對映到目標機器

(3) 決定值是否應該儲存在暫存器或記憶體中,以及值可以在哪裡檢索讀取


這是程式碼生成器執行 LHS 和 RHS 查詢的環節。簡而言之,LHS 查詢會將目標值寫入記憶體,RHS 查詢會從記憶體中讀取目標值。

如果值既被存入記憶體又被存入暫存器,程式碼生成器就會從暫存器中取值來進行優化。從記憶體中取值是最次選擇。


到了最後……

(4) 決定了指令的執行順序。

[譯] 漫畫圖解 JavaScript 引擎: let jsCartoons = ‘Awesome’;

最後的一點思考

理解 JavaScript 引擎的另一個方法是看看你的 大腦。當你讀到這裡,你的大腦正在從視網膜獲取資料。通過視神經傳遞的資料是網頁的翻轉版本,為了能解釋影象,你的大腦會通過反轉它來進行編譯。

除了翻轉影象並著色之外,大腦可以根據識別模式的能力來填充空格,就像編譯器從快取中讀取資料一樣。

因此如果我們寫下 _please give us a round of _____, 這句話,你就很容易地執行這段程式碼。


code in peace

Raji Ayinla,

科技內容實習作家 @ Codesmith Staffing

參考內容


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章