從一個誤寫的逗號談開去——JS程式碼是如何被壓縮的

這是你的玩具車嗎發表於2020-06-01

故事起源於一個很小問題,我寫了個程式碼,被質疑有問題:簡化之後大概如下:

let a;
const x = { b: 123 };
a = 123,
delete x

被質疑的主要原因是第三行a=123的後面為什麼是逗號,不是分號。坦白來說,我是簡單的手誤,將分號錯寫成了逗號。但是感覺貌似應該也沒有什麼問題,畢竟uglifyjs會將某些語句進行合併,將分號變成逗號。繼而再一想,uglifyjs是如何來進行程式碼壓縮的、它是如何知道該合併哪些語句,不合並哪些語句的、 它又有哪些合併規則?於是有了本文。

1. AST(抽象語法樹)

要想了解JS的壓縮原理,需要首先了解AST。

抽象語法樹:AST(Abstract Syntax Tree),是原始碼的抽象語法結構的樹狀表現形式,這裡特指程式語言的原始碼。樹上的每個節點都表示原始碼中的一種結構。之所以說語法是「抽象」的,是因為這裡的語法並不會表示出真實語法中出現的每個細節。

舉個例子:

image.png

image.png

從上面兩個例子中,可以看出AST是原始碼根據其語法結構,省略一些細節(比如:括號沒有生成節點),抽象成樹形表達。抽象語法樹在電腦科學中有很多應用,比如編譯器、IDE、壓縮程式碼、格式化程式碼等。[1]

2. 程式碼壓縮原理

瞭解了AST之後,我們再分析一下JS的程式碼壓縮原理。簡單的說,就是

1. 將code轉換成AST
2. 將AST進行優化,生成一個更小的AST
3. 將新生成的AST再轉化成code

PS:具體的AST樹大家可以在astexplorer上線上獲得

babel,eslint,v8的邏輯均與此類似,下圖是我們引用了babel的轉化示意圖:
1.jpg

以我們之前被質疑的程式碼為例,看看它在uglify中是怎麼樣一步一步被壓縮的:

// uglify-js的版本需要為2.x, 3.0之後uglifyjs不再暴露Compressor api
// 2.x的uglify不能自動解析es6,所以這裡先切換成es5
// npm install uglify-js@2.x
var UglifyJS = require('uglify-js');

// 原始程式碼
var code = `var a;
var x = { b: 123 };
a = 123,
delete x`;

// 通過 UglifyJS 把程式碼解析為 AST
var ast = UglifyJS.parse(code);
ast.figure_out_scope();

// 轉化為一顆更小的 AST 樹
compressor = UglifyJS.Compressor();
ast = ast.transform(compressor);

// 再把 AST 轉化為程式碼
code = ast.print_to_string();

// var a,x={b:123};a=123,delete x;
console.log("code", code);

到這裡,我們已經瞭解了uglifyjs的程式碼壓縮原理,但是還沒有解決一個問題——為什麼某些語句間的分號會被轉換為逗號,某些不會轉換。這就涉及到了uglifyjs的壓縮規則。

3. 程式碼壓縮規則

由於uglifyjs的程式碼壓縮規則很多,我們這裡只分析與本文中相關的部分:

uglifyjs的全部壓縮規則可以參見:《[解讀uglifyJS(四)——Javascript程式碼壓縮](https://rapheal.sinaapp.com/2014/05/22/uglifyjs-squeeze/#more-705)》
連續的"表示式語句"可以合併成一個逗號表示式

image.png

PS:線上demo

這其中需要注意的是隻有“表示式語句”才能被合併,那麼什麼是表示式語句呢?

表示式 VS 語句 VS 表示式語句

表示式:表示式都會返回一個值,可以放在任何一個需要值的地方

例如:

    a; //返回a的值
    b + 3; // 返回b+3的結果
語句:語句是一個行為,通常利用一個或多個關鍵字來完成給定的任務。程式由一系列語句構成。其中流控制語句有:if/while/for等。

例如:

    if(x > 0) {
      ...
    }
    for(var i = 0;i < arr.length; i ++) {
      ...
    }
    const a = 123;
表示式語句:既是表示式,又是語句

例如:

    A();
    function() {}();
    delete x.b;
    b = b + 3;

綜上所述,因為a = 123 和 delete x都是表示式語句,所以分號被轉換為逗號。而var x = {b:123}則因為是宣告語句,所以和a=123不會合並,分號不會被轉換。但var x = {b:123}和第一行var a又觸發了另外一條規則,

多個var宣告可以壓縮成一個var宣告

所以第一行和第二行會被合併為var a,x={b:123}

4. 總結

在本文中,我們討論了什麼是抽象語法樹,uglifyjs的壓縮原理,以及相應的壓縮規則,最終明晰了為什麼程式碼會被壓縮成我們得到的樣子,希望對大家有所幫助。

參考文獻

[1]《抽象語法樹在 JavaScript 中的應用
[2]《javascript 程式碼是如何被壓縮的
[3]《[譯]JavaScript中:表示式和語句的區別
[4]《解讀uglifyJS(四)——Javascript程式碼壓縮

相關文章