前言
最近給團隊分享了一篇babel原理,然後我把他整理成一篇blog,本篇總字數6059(含程式碼),速讀3分鐘,普通閱讀5分鐘,有興趣的可以關注一下我的github部落格
babel
我們來看一段程式碼:
[1,2,3].map(n => n + 1);
複製程式碼
經過babel之後,這段程式碼變成了這樣:
[1, 2, 3].map(function (n) {
return n + 1;
});
複製程式碼
babel的背後
babel的過程:解析——轉換——生成。
這邊又一箇中間的東西,是抽象語法樹(AST)
AST的解析過程
一個js語句是怎麼被解析成AST的呢?這個中間有兩個步驟,一個是分詞,第二個是語義分析,怎麼理解這兩個東西呢?
- 分詞
什麼叫分詞?
比如我們在讀一句話的時候,我們也會做分詞操作,比如:“今天天氣真好”,我們會把他切割成“今天”,“天氣”,“真好”。
那換成js的解析器呢,我們看一下下面一個語句console.log(1);
,js會看成console
,.
,log
,(
,1
,)
,;
。
所以我們可以把js解析器能識別的最小詞法單元。
當然這樣的分詞器我們可以簡易實現一下。
//思路分析:傳入的是字串的引數,然後每次取一個字元去校驗,用if語句去判斷,然後最後結果存入一個陣列中,對於識別符號和數字進行特殊處理
function tokenCode(code) {
const tokens = [];
//字串的迴圈
for(let i = 0; i < code.length; i++) {
let currentChar = code.charAt(i);
//是分號括號的情況
if (currentChar === ';' || currentChar === '(' || currentChar === ')' || currentChar === '}' || currentChar === '{' || currentChar === '.' || currentChar === '=') {
// 對於這種只有一個字元的語法單元,直接加到結果當中
tokens.push({
type: 'Punctuator',
value: currentChar,
});
continue;
}
//是運算子的情況
if (currentChar === '>' || currentChar === '<' || currentChar === '+' || currentChar === '-') {
// 與上一步類似只是語法單元型別不同
tokens.push({
type: 'operator',
value: currentChar,
});
continue;
}
//是雙引號或者單引號的情況
if (currentChar === '"' || currentChar === '\'') {
// 引號表示一個字元傳的開始
const token = {
type: 'string',
value: currentChar, // 記錄這個語法單元目前的內容
};
tokens.push(token);
const closer = currentChar;
// 進行巢狀迴圈遍歷,尋找字串結尾
for (i++; i < code.length; i++) {
currentChar = code.charAt(i);
// 先將當前遍歷到的字元無條件加到字串的內容當中
token.value += currentChar;
if (currentChar === closer) {
break;
}
}
continue;
}
if (/[0-9]/.test(currentChar)) {
// 數字是以0到9的字元開始的
const token = {
type: 'number',
value: currentChar,
};
tokens.push(token);
for (i++; i < code.length; i++) {
currentChar = code.charAt(i);
if (/[0-9\.]/.test(currentChar)) {
// 如果遍歷到的字元還是數字的一部分(0到9或小數點)
// 這裡暫不考慮會出現多個小數點以及其他進位制的情況
token.value += currentChar;
} else {
// 遇到不是數字的字元就退出,需要把 i 往回撥,
// 因為當前的字元並不屬於數字的一部分,需要做後續解析
i--;
break;
}
}
continue;
}
if (/[a-zA-Z\$\_]/.test(currentChar)) {
// 識別符號是以字母、$、_開始的
const token = {
type: 'identifier',
value: currentChar,
};
tokens.push(token);
// 與數字同理
for (i++; i < code.length; i++) {
currentChar = code.charAt(i);
if (/[a-zA-Z0-9\$\_]/.test(currentChar)) {
token.value += currentChar;
} else {
i--;
break;
}
}
continue;
}
if (/\s/.test(currentChar)) {
// 連續的空白字元組合到一起
const token = {
type: 'whitespace',
value: currentChar,
};
// 與數字同理
for (i++; i < code.length; i++) {
currentChar = code.charAt(i);
if (/\s]/.test(currentChar)) {
token.value += currentChar;
} else {
i--;
break;
}
}
continue;
}
throw new Error('Unexpected ' + currentChar);
}
return tokens;
}
複製程式碼
- 語義分析
語義分析的話就比較難了,為什麼這麼說呢?
因為這個不像分詞這樣有個標準,有些東西都要靠自己去摸索。
其實語義分析分為兩塊,一塊是語句,還有一塊是表示式。
什麼叫語句?什麼叫表示式呢?
表示式,比如:a > b; a + b;
這一類的,可以巢狀,也可以運用在語句中。
語句,比如:var a = 1, b = 2, c =3;
等,我們理解中的一個語句。類似於語文中的一個句子一樣。
當然,有人會問,console.log(1);
這個算什麼呢。
其實這種情況可以歸為一類,單語句表示式,你既可以看作表示式,也可以看作語句,一個表示式單成一個語句。
既然分完了,我們也可以嘗試這來寫一下,簡單點的語句分析。 比如var定義語句,或者複雜點的if語句塊。
生成AST的形式可以參考這個網站,AST的一些語法可以從這個網站試出個大概
//思路分析:既然分三種情況,那麼我們也從語句,表示式,單語句表示式入手,我們先定義一個方法用來分析表示式,在定義一個方法來分析語句,最後在定義一個方法分析單語句表示式。整個過程也是分為那麼幾步。就多了對於指標的管控。
function parse (tokens) {
// 位置暫存棧,用於支援很多時候需要返回到某個之前的位置
const stashStack = [];
let i = -1; // 用於標識當前遍歷位置
let curToken; // 用於記錄當前符號
// 暫存當前位置
function stash () {
stashStack.push(i);
}
// 往後移動讀取指標
function nextToken () {
i++;
curToken = tokens[i] || { type: 'EOF' };;
}
function parseFalse () {
// 解析失敗,回到上一個暫存的位置
i = stashStack.pop();
curToken = tokens[i];
}
function parseSuccess () {
// 解析成功,不需要再返回
stashStack.pop();
}
const ast = {
type: 'Program',
body: [],
sourceType: "script"
};
// 讀取下一個語句
function nextStatement () {
// 暫存當前的i,如果無法找到符合條件的情況會需要回到這裡
stash();
// 讀取下一個符號
nextToken();
if (curToken.type === 'identifier' && curToken.value === 'if') {
// 解析 if 語句
const statement = {
type: 'IfStatement',
};
// if 後面必須緊跟著 (
nextToken();
if (curToken.type !== 'Punctuator' || curToken.value !== '(') {
throw new Error('Expected ( after if');
}
// 後續的一個表示式是 if 的判斷條件
statement.test = nextExpression();
// 判斷條件之後必須是 )
nextToken();
if (curToken.type !== 'Punctuator' || curToken.value !== ')') {
throw new Error('Expected ) after if test expression');
}
// 下一個語句是 if 成立時執行的語句
statement.consequent = nextStatement();
// 如果下一個符號是 else 就說明還存在 if 不成立時的邏輯
if (curToken === 'identifier' && curToken.value === 'else') {
statement.alternative = nextStatement();
} else {
statement.alternative = null;
}
parseSuccess();
return statement;
}
// 如果是花括號的程式碼塊
if (curToken.type === 'Punctuator' && curToken.value === '{') {
// 以 { 開頭表示是個程式碼塊
const statement = {
type: 'BlockStatement',
body: [],
};
while (i < tokens.length) {
// 檢查下一個符號是不是 }
stash();
nextToken();
if (curToken.type === 'Punctuator' && curToken.value === '}') {
// } 表示程式碼塊的結尾
parseSuccess();
break;
}
// 還原到原來的位置,並將解析的下一個語句加到body
parseFalse();
statement.body.push(nextStatement());
}
// 程式碼塊語句解析完畢,返回結果
parseSuccess();
return statement;
}
// 沒有找到特別的語句標誌,回到語句開頭
parseFalse();
// 嘗試解析單表示式語句
const statement = {
type: 'ExpressionStatement',
expression: nextExpression(),
};
if (statement.expression) {
nextToken();
return statement;
}
}
// 讀取下一個表示式
function nextExpression () {
nextToken();
if (curToken.type === 'identifier' && curToken.value === 'var') {
// 如果是定義var
const variable = {
type: 'VariableDeclaration',
declarations: [],
kind: curToken.value
};
stash();
nextToken();
// 如果是分號就說明單句結束了
if(curToken.type === 'Punctuator' && curToken.value === ';') {
parseSuccess();
throw new Error('error');
} else {
// 迴圈
while (i < tokens.length) {
if(curToken.type === 'identifier') {
variable.declarations.id = {
type: 'Identifier',
name: curToken.value
}
}
if(curToken.type === 'Punctuator' && curToken.value === '=') {
nextToken();
variable.declarations.init = {
type: 'Literal',
name: curToken.value
}
}
nextToken();
// 遇到;結束
if (curToken.type === 'Punctuator' && curToken.value === ';') {
break;
}
}
}
parseSuccess();
return variable;
}
// 常量表示式
if (curToken.type === 'number' || curToken.type === 'string') {
const literal = {
type: 'Literal',
value: eval(curToken.value),
};
// 但如果下一個符號是運算子
// 此處暫不考慮多個運算銜接,或者有變數存在
stash();
nextToken();
if (curToken.type === 'operator') {
parseSuccess();
return {
type: 'BinaryExpression',
operator: curToken.value,
left: literal,
right: nextExpression(),
};
}
parseFalse();
return literal;
}
if (curToken.type !== 'EOF') {
throw new Error('Unexpected token ' + curToken.value);
}
}
// 逐條解析頂層語句
while (i < tokens.length) {
const statement = nextStatement();
if (!statement) {
break;
}
ast.body.push(statement);
}
return ast;
}
複製程式碼
關於轉換和生成,筆者還在研究,不過生成其實就是解析過程的反向,轉換的話,還是挺值得深入的,因為AST這東西在好多方面用到,比如:
- eslint對程式碼錯誤或風格的檢查,發現一些潛在的錯誤
- IDE的錯誤提示、格式化、高亮、自動補全等
- UglifyJS壓縮程式碼
- 程式碼打包工具webpack
這篇文章講完了,其實不理解程式碼沒關係,把整體思路把握住就行。