通過DLS解析器,了解JS的編譯原理

接觸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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容