接觸Javascript很長一段時間了,但一直浮在語言的表面,今天決定重頭開始更深入的學習Javascript,先從Javascript的編譯原理開始。
在程序的執(zhí)行方式中有編譯型和解釋型,以前學習的C語言就是編譯型語言,它需要提前把所有源代碼翻譯成機器能識別的指令進行運行。JavaScript就是解釋型語言,它可以翻譯一條執(zhí)行一條。如何更通俗的理解編譯和解釋的區(qū)別呢?打個比喻:編譯相當于做好一桌子菜再開吃; 解釋就是吃火鍋。
盡管通常將Javascript歸類為“動態(tài)”或“稀釋執(zhí)行”語言,但事實上他是一門編譯語言,但與傳統(tǒng)的編譯語言不同,它不是提前編譯的,編譯的結果也不能在分布式系統(tǒng)中進行移植。例如像V8(Chrome的JS引擎),它其實為了提高JS的運行性能,在運行之前會先將JS編譯為本地的機器碼,然后再去執(zhí)行機器碼。
一段源代碼在執(zhí)行之前經歷三個步驟:
- 分詞/詞法分析
- 解析/語法分析
- 代碼生成
分詞/詞法分析(Tokenizing/Lexing)
將字符串分解成有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。例如:var a = 2,這段程序的詞法單元就是:var、a、=、2。
解析/語法分析(Parsing)
將詞法單元(數(shù)組)轉換成一個由元素逐級嵌套所組成的代表了程序語法結構的數(shù)。這個數(shù)稱為“抽象語法樹”(Abstract Syntax Tree),簡稱"AST"。
代碼生成
將AST轉換為可執(zhí)行代碼的過程稱為代碼生成。
以上只是進行宏觀、簡單的介紹,JavaScript引擎實際上做的事情比這個復雜的多,在JIT(Just-in-time)就是對代碼進行各種優(yōu)化。
其實理解編譯的過程,自己動手實現(xiàn)一個簡單的編譯器能更好的理解這個過程。下面是一個用戶JavaScript實現(xiàn)的簡單編譯器:
/**
* sum 加
* sub 減
* mul 乘
* div 除
* ##### 以下是我們編譯器要實現(xiàn)的原理 ###############
* (sum 3 3) 2+2
* (sub 3 2) 3-2
* (mul (sum 3 3) (sub 3 2)) (3+3)*(3-2)
* (div (mul (sum 3 3) (sub 3 2)) 1) ((3+3)*(3-2))/1
*/
/**
* 分詞/詞法分析
* @argument {string} str 接收一個代碼組成的字符串
* @return {array} 返回一個詞法單元組成的數(shù)組
*/
function tokenizer(str) {
// 當前字符的位置
let current = 0;
// 存放詞法單元的敵法
const tokens = [];
const regSpace = /\s/;
const regNumber = /[0-9]/;
const regLetter = /[a-z]/i;
const len = str.length;
while (current < len) {
let char = str[current];
if (char === '(') {
tokens.push({
type: 'paren',
value: '('
});
current++;
continue;
}
if (char === ')') {
tokens.push({
type: 'paren',
value: ')'
});
current++;
continue;
}
// 檢測類型為空
if (regSpace.test(char)) {
current++;
continue;
}
// 檢測類型為數(shù)值
if (regNumber.test(char)) {
let value = '';
while (regNumber.test(char)) {
value += char;
char = str[++current];
}
tokens.push({
type: 'number',
value: value
});
continue;
}
// 檢測類型為字符串
if (regLetter.test(char)) {
let value = '';
while (regLetter.test(char)) {
value += char;
char = str[++current];
}
tokens.push({
type: 'operator',
value: value,
});
continue;
}
throw new TypeError('I dont know what this character is: ' + char);
}
return tokens;
}
/**
* 語法分析器
* @argument {array} 接收一個詞法單元(token)組成的數(shù)組
* @return {object} 返回一個把詞法單元流轉換成一個“抽象語法樹”AST
*/
function parser(tokens) {
let current = 0;
function parse() {
let token = tokens[current];
if (token.type === 'number') {
current++;
return {
type: 'NumberLiteral',
value: token.value
};
}
if ( token.type === 'paren' && token.value === '(') {
token = tokens[++current];
const node = {
type: 'CallExpression',
name: token.value,
params: []
};
token = tokens[++current];
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
node.params.push(parse());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type)
}
return parse();
}
/** 將抽象語法樹生成代碼
* @argument {object} 抽象語法樹
* @return {string} 可執(zhí)行代碼字符串
*/
function codeGenerator(ast) {
const operator = {
sum: '+',
sub: '-',
mul: '*',
div: '/'
};
const compileNum = ast => ast.value;
const compileOp = ast => `(${ast.params.map(compile).join(' ' + operator[ast.name] + ' ')})`;
const compile = ast => ast.type === 'NumberLiteral' ? compileNum(ast) : compileOp(ast);
return compile(ast);
}
// 編譯器:詞法分析 + 語法分析 + 代碼生成
function compiler(code) {
return codeGenerator(parser(tokenizer(code)));
}
const codeResult = compiler('(div (mul (sum 3 3) (sub 3 2)) 1) ')
console.log(codeResult) //=> (((3 + 3) * (3 - 2)) / 1)
console.log(eval(codeResult)) //=> 6
上面的解析器,將詞法分析器把詞法單元分類: "paren", "number", "operator"三種類型進行存儲,再通過語法分析器將數(shù)值和運算符區(qū)分出來,通過遞歸解析把詞法單元流轉換成AST,最后通過代碼生成器,根據AST中節(jié)點的類型轉換可執(zhí)行的代碼。
總結
通過實現(xiàn)一個簡單的DLS解釋器,我們能宏觀的了解JavaScript在代碼執(zhí)行前幾微秒發(fā)生的編譯過程,當然這只是最簡單的介紹,具體可以了解去JIT做了什么。
參考文獻
《你不知道的JavaScript》
用25行JavaScript語句實現(xiàn)一個簡單的編譯器
the-super-tiny-compiler-cn