<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Babel是如何讀懂JS代碼的

          共 4140字,需瀏覽 9分鐘

           ·

          2020-09-04 15:14

          編者按:本文轉載自安秦的知乎文章,快來一起學習吧!

          概述

          本文不再介紹Babel是什么也不講怎么用,這類文章很多,我也不覺得自己能寫得更好。這篇文章的關注點是另一個方面,也是很多人會好奇的事情,Babel的工作原理是什么。

          Babel工作的三個階段

          首先要說明的是,現(xiàn)在前端流行用的WebPack或其他同類工程化工具會將源文件組合起來,這部分并不是Babel完成的,是這些打包工具自己實現(xiàn)的,Babel的功能非常純粹,以字符串的形式將源代碼傳給它,它就會返回一段新的代碼字符串(以及sourcemap)。他既不會運行你的代碼,也不會將多個代碼打包到一起,它就是個編譯器,輸入語言是ES6+,編譯目標語言是ES5。

          在Babel官網(wǎng),plugins菜單下藏著一個鏈接:thejameskyle/the-super-tiny-compiler。它已經(jīng)解釋了整個工作過程,有耐心者可以自己研究,當然也可以繼續(xù)看我的文章。

          Babel的編譯過程跟絕大多數(shù)其他語言的編譯器大致同理,分為三個階段:

          1. 解析:將代碼字符串解析成抽象語法樹

          2. 變換:對抽象語法樹進行變換操作

          3. 再建:根據(jù)變換后的抽象語法樹再生成代碼字符串


          像我們在.babelrc里配置的presets和plugins都是在第2步工作的。

          舉個例子,首先你輸入的代碼如下:

          if (1 > 0) {
          alert('hi');
          }

          經(jīng)過第1步得到一個如下的對象:

          {
          "type": "Program", // 程序根節(jié)點
          "body": [ // 一個數(shù)組包含所有程序的頂層語句
          {
          "type": "IfStatement", // 一個if語句節(jié)點
          "test": { // if語句的判斷條件
          "type": "BinaryExpression", // 一個雙元運算表達式節(jié)點
          "operator": ">", // 運算表達式的運算符
          "left": { // 運算符左側值
          "type": "Literal", // 一個常量表達式
          "value": 1 // 常量表達式的常量值
          },
          "right": { // 運算符右側值
          "type": "Literal",
          "value": 0
          }
          },
          "consequent": { // if語句條件滿足時的執(zhí)行內(nèi)容
          "type": "BlockStatement", // 用{}包圍的代碼塊
          "body": [ // 代碼塊內(nèi)的語句數(shù)組
          {
          "type": "ExpressionStatement", // 一個表達式語句節(jié)點
          "expression": {
          "type": "CallExpression", // 一個函數(shù)調(diào)用表達式節(jié)點
          "callee": { // 被調(diào)用者
          "type": "Identifier", // 一個標識符表達式節(jié)點
          "name": "alert"
          },
          "arguments": [ // 調(diào)用參數(shù)
          {
          "type": "Literal",
          "value": "hi"
          }
          ]
          }
          }
          ]
          },
          "alternative": null // if語句條件未滿足時的執(zhí)行內(nèi)容
          }
          ]
          }

          Babel實際生成的語法樹還會包含更多復雜信息,這里只展示比較關鍵的部分,欲了解更多關于ES語言抽象語法樹規(guī)范可閱讀:The ESTree Spec。

          用圖像更簡單地表達上面的結構:



          第1步轉換的過程中可以驗證語法的正確性,同時由字符串變?yōu)閷ο蠼Y構后更有利于精準地分析以及進行代碼結構調(diào)整。

          第2步原理就很簡單了,就是遍歷這個對象所描述的抽象語法樹,遇到哪里需要做一下改變,就直接在對象上進行操作,比如我把IfStatement給改成WhileStatement就達到了把條件判斷改成循環(huán)的效果。

          第3步也簡單,遞歸遍歷這顆語法樹,然后生成相應的代碼,大概的實現(xiàn)邏輯如下:

          const types = {
          Program (node) {
          return node.body.map(child => generate(child));
          },
          IfStatement (node) {
          let code = `if (${generate(node.test)}) ${generate(node.consequent)}`;
          if (node.alternative) {
          code += `else ${generate(node.alternative)}`;
          }
          return code;
          },
          BlockStatement (node) {
          let code = node.body.map(child => generate(child));
          code = `{ ${code} }`;
          return code;
          },
          ......
          };
          function generate(node) {
          return types[node.type](node);
          }
          const ast = Babel.parse(...); // 將代碼解析成語法樹
          const generatedCode = generate(ast); // 將語法樹重新組合成代碼

          抽象語法樹是如何產(chǎn)生的

          第2、3步相信不用花多少篇幅大家自己都能理解,重點介紹的第一步來了。

          解析這一步又分成兩個步驟:

          1. 分詞:將整個代碼字符串分割成?語法單元?數(shù)組

          2. 語義分析:在分詞結果的基礎之上分析?語法單元之間的關系

          我們一步步講。

          分詞

          首先解釋一下什么是語法單元:語法單元是被解析語法當中具備實際意義的最小單元,通俗點說就是類似于自然語言中的詞語。

          看這句話“2020年奧運會將在東京舉行”,不論詞性及主謂關系等,人第一步會把這句話拆分成:2020年、奧運會、將、在、東京、舉行。這就是分詞:把整句話拆分成有意義的最小顆粒,這些小塊不能再被拆分,否則就失去它所能表達的意義了。

          那么回到代碼的解析當中,JS代碼有哪些語法單元呢?大致有以下這些(其他語言也許類似但通常都有區(qū)別):

          • 空白:JS中連續(xù)的空格、換行、縮進等這些如果不在字符串里,就沒有任何實際邏輯意義,所以把連續(xù)的空白符直接組合在一起作為一個語法單元。

          • 注釋:行注釋或塊注釋,雖然對于人類來說有意義,但是對于計算機來說知道這是個“注釋”就行了,并不關心內(nèi)容,所以直接作為一個不可再拆的語法單元

          • 字符串:對于機器而言,字符串的內(nèi)容只是會參與計算或展示,里面再細分的內(nèi)容也是沒必要分析的

          • 數(shù)字:JS語言里就有16、10、8進制以及科學表達法等數(shù)字表達語法,數(shù)字也是個具備含義的最小單元

          • 標識符:沒有被引號擴起來的連續(xù)字符,可包含字母、_、$、及數(shù)字(數(shù)字不能作為開頭)。標識符可能代表一個變量,或者true、false這種內(nèi)置常量、也可能是if、return、function這種關鍵字,是哪種語義,分詞階段并不在乎,只要正確切分就好了。

          • 運算符:+、-、*、/、>、<等等

          • 括號:(...)可能表示運算優(yōu)先級、也可能表示函數(shù)調(diào)用,分詞階段并不關注是哪種語義,只把“(”或“)”當做一種基本語法單元

          • 還有其他:如中括號、大括號、分號、冒號、點等等不再一一列舉

          分詞的過過程從邏輯來講并不難解釋,但是這是個精細活,要考慮清楚所有的情況。還是以一個代碼為例:

          if (1 > 0) {
          alert("if \"1 > 0\"");
          }

          我們希望得到的分詞是:

          'if'     ' '       '('    '1'      ' '    '>'    ' '    ')'    ' '    '{'
          '\n ' 'alert' '(' '"if \"1 > 0\""' ')' ';' '\n' '}'

          注意其中"if \"1 > 0\""是作為一個語法單元存在,沒有再查分成if、1、>、0這樣,而且其中的轉譯符會阻止字符串早結束。

          這拆分過程其實沒啥可取巧的,就是簡單粗暴地一個字符一個字符地遍歷,然后分情況討論,整個實現(xiàn)方法就是順序遍歷和大量的條件判斷。我用一個簡單的實現(xiàn)來解釋,在關鍵的地方注釋,我們只考慮上面那段代碼里存在的語法單元類型。

          function tokenizeCode (code) {
          const tokens = []; // 結果數(shù)組
          for (let i = 0; i < code.length; i++) {
          // 從0開始,一個字符一個字符地讀取
          let currentChar = code.charAt(i);

          if (currentChar === ';') {
          // 對于這種只有一個字符的語法單元,直接加到結果當中
          tokens.push({
          type: 'sep',
          value: ';',
          });
          // 該字符已經(jīng)得到解析,不需要做后續(xù)判斷,直接開始下一個
          continue;
          }

          if (currentChar === '(' || currentChar === ')') {
          // 與 ; 類似只是語法單元類型不同
          tokens.push({
          type: 'parens',
          value: currentChar,
          });
          continue;
          }

          if (currentChar === '}' || currentChar === '{') {
          // 與 ; 類似只是語法單元類型不同
          tokens.push({
          type: 'brace',
          value: currentChar,
          });
          continue;
          }

          if (currentChar === '>' || currentChar === '<') {
          // 與 ; 類似只是語法單元類型不同
          tokens.push({
          type: 'operator',
          value: currentChar,
          });
          continue;
          }

          if (currentChar === '"' || currentChar === '\'') {
          // 引號表示一個字符傳的開始
          const token = {
          type: 'string',
          value: currentChar, // 記錄這個語法單元目前的內(nèi)容
          };
          tokens.push(token);

          const closer = currentChar;
          let escaped = false; // 表示下一個字符是不是被轉譯的

          // 進行嵌套循環(huán)遍歷,尋找字符串結尾
          for (i++; i < code.length; i++) {
          currentChar = code.charAt(i);
          // 先將當前遍歷到的字符無條件加到字符串的內(nèi)容當中
          token.value += currentChar;
          if (escaped) {
          // 如果當前轉譯狀態(tài)是true,就將改為false,然后就不特殊處理這個字符
          escaped = false;
          } else if (currentChar === '\\') {
          // 如果當前字符是 \ ,將轉譯狀態(tài)設為true,下一個字符不會被特殊處理
          escaped = true;
          } else if (currentChar === closer) {
          break;
          }
          }
          continue;
          }

          if (/[0-9]/.test(currentChar)) {
          // 數(shù)字是以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)) {
          // 如果遍歷到的字符還是數(shù)字的一部分(0到9或小數(shù)點)
          // 這里暫不考慮會出現(xiàn)多個小數(shù)點以及其他進制的情況
          token.value += currentChar;
          } else {
          // 遇到不是數(shù)字的字符就退出,需要把 i 往回調(diào),
          // 因為當前的字符并不屬于數(shù)字的一部分,需要做后續(xù)解析
          i--;
          break;
          }
          }
          continue;
          }

          if (/[a-zA-Z\$\_]/.test(currentChar)) {
          // 標識符是以字母、$、_開始的
          const token = {
          type: 'identifier',
          value: currentChar,
          };
          tokens.push(token);

          // 與數(shù)字同理
          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)) {
          // 連續(xù)的空白字符組合到一起
          const token = {
          type: 'whitespace',
          value: currentChar,
          };
          tokens.push(token);

          // 與數(shù)字同理
          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;
          }

          const tokens = tokenizeCode(`
          if (1 > 0) {
          alert("if 1 > 0");
          }
          `);

          以上代碼是我個人的實現(xiàn)方式,與babel實際略有不同,但主要思路一樣。

          執(zhí)行結果如下:

          [
          { type: "whitespace", value: "\n" },
          { type: "identifier", value: "if" },
          { type: "whitespace", value: " " },
          { type: "parens", value: "(" },
          { type: "number", value: "1" },
          { type: "whitespace", value: " " },
          { type: "operator", value: ">" },
          { type: "whitespace", value: " " },
          { type: "number", value: "0" },
          { type: "parens", value: ")" },
          { type: "whitespace", value: " " },
          { type: "brace", value: "{" },
          { type: "whitespace", value: "\n " },
          { type: "identifier", value: "alert" },
          { type: "parens", value: "(" },
          { type: "string", value: "\"if 1 > 0\"" },
          { type: "parens", value: ")" },
          { type: "sep", value: ";" },
          { type: "whitespace", value: "\n" },
          { type: "brace", value: "}" },
          { type: "whitespace", value: "\n" },
          ]

          經(jīng)過這一步的分詞,這個數(shù)組就比攤開的字符串更方便進行下一步處理了。

          語義分析

          語義分析就是把詞匯進行立體的組合,確定有多重意義的詞語最終是什么意思、多個詞語之間有什么關系以及又應該再哪里斷句等。

          在編程語言解釋當中,這就是要最終生成語法樹的步驟了。不像自然語言,像“從句”這種結構往往最多只有一層,編程語言的各種從屬關系更加復雜。

          在編程語言的解析中有兩個很相似但是又有區(qū)別的重要概念:

          • 語句:語句是一個具備邊界的代碼區(qū)域,相鄰的兩個語句之間從語法上來講互不干擾,調(diào)換順序雖然可能會影響執(zhí)行結果,但不會產(chǎn)生語法錯誤
            比如return true、var a = 10、if (...) {...}

          • 表達式:最終有個結果的一小段代碼,它的特點是可以原樣嵌入到另一個表達式
            比如myVar、1+1、str.replace('a', 'b')、i < 10 && i > 0等

          很多情況下一個語句可能只包含一個表達式,比如console.log('hi');。estree標準當中,這種語句節(jié)點稱作ExpressionStatement。

          語義分析的過程又是個遍歷語法單元的過程,不過相比較而言更復雜,因為分詞過程中,每個語法單元都是獨立平鋪的,而語法分析中,語句和表達式會以樹狀的結構互相包含。針對這種情況我們可以用棧,也可以用遞歸來實現(xiàn)。

          我繼續(xù)上面的例子給出語義分析的代碼,代碼很長,先在最開頭說明幾個函數(shù)是做什么的:

          • nextStatement:讀取并返回下一個語句

          • nextExpression:讀取并返回下一個表達式

          • nextToken:讀取下一個語法單元(或稱符號),賦值給curToken

          • stash:暫存當前讀取符號的位置,方便在需要的時候返回

          • rewind:返回到上一個暫存點

          • commit:上一個暫存點不再被需要,將其銷毀

          這里stash、rewind、commit都跟讀取位置暫存相關,什么樣的情況會需要返回到暫存點呢?有時同一種語法單元有可能代表不同類型的表達式的開始。先stash,然后按照其中一種嘗試解析,如果解析成功了,那么暫存點就沒用了,commit將其銷毀。如果解析失敗了,就用rewind回到原來的位置再按照另一種方式嘗試去解析。

          以下是代碼:


          function parse (tokens) {
          let i = -1; // 用于標識當前遍歷位置
          let curToken; // 用于記錄當前符號

          // 讀取下一個語句
          function nextStatement () {
          // 暫存當前的i,如果無法找到符合條件的情況會需要回到這里
          stash();

          // 讀取下一個符號
          nextToken();

          if (curToken.type === 'identifier' && curToken.value === 'if') {
          // 解析 if 語句
          const statement = {
          type: 'IfStatement',
          };
          // if 后面必須緊跟著 (
          nextToken();
          if (curToken.type !== 'parens' || curToken.value !== '(') {
          throw new Error('Expected ( after if');
          }

          // 后續(xù)的一個表達式是 if 的判斷條件
          statement.test = nextExpression();

          // 判斷條件之后必須是 )
          nextToken();
          if (curToken.type !== 'parens' || curToken.value !== ')') {
          throw new Error('Expected ) after if test expression');
          }

          // 下一個語句是 if 成立時執(zhí)行的語句
          statement.consequent = nextStatement();

          // 如果下一個符號是 else 就說明還存在 if 不成立時的邏輯
          if (curToken === 'identifier' && curToken.value === 'else') {
          statement.alternative = nextStatement();
          } else {
          statement.alternative = null;
          }
          commit();
          return statement;
          }

          if (curToken.type === 'brace' && curToken.value === '{') {
          // 以 { 開頭表示是個代碼塊,我們暫不考慮JSON語法的存在
          const statement = {
          type: 'BlockStatement',
          body: [],
          };
          while (i < tokens.length) {
          // 檢查下一個符號是不是 }
          stash();
          nextToken();
          if (curToken.type === 'brace' && curToken.value === '}') {
          // } 表示代碼塊的結尾
          commit();
          break;
          }
          // 還原到原來的位置,并將解析的下一個語句加到body
          rewind();
          statement.body.push(nextStatement());
          }
          // 代碼塊語句解析完畢,返回結果
          commit();
          return statement;
          }

          // 沒有找到特別的語句標志,回到語句開頭
          rewind();

          // 嘗試解析單表達式語句
          const statement = {
          type: 'ExpressionStatement',
          expression: nextExpression(),
          };
          if (statement.expression) {
          nextToken();
          if (curToken.type !== 'EOF' && curToken.type !== 'sep') {
          throw new Error('Missing ; at end of expression');
          }
          return statement;
          }
          }

          // 讀取下一個表達式
          function nextExpression () {
          nextToken();

          if (curToken.type === 'identifier') {
          const identifier = {
          type: 'Identifier',
          name: curToken.value,
          };
          stash();
          nextToken();
          if (curToken.type === 'parens' && curToken.value === '(') {
          // 如果一個標識符后面緊跟著 ( ,說明是個函數(shù)調(diào)用表達式
          const expr = {
          type: 'CallExpression',
          caller: identifier,
          arguments: [],
          };

          stash();
          nextToken();
          if (curToken.type === 'parens' && curToken.value === ')') {
          // 如果下一個符合直接就是 ) ,說明沒有參數(shù)
          commit();
          } else {
          // 讀取函數(shù)調(diào)用參數(shù)
          rewind();
          while (i < tokens.length) {
          // 將下一個表達式加到arguments當中
          expr.arguments.push(nextExpression());
          nextToken();
          // 遇到 ) 結束
          if (curToken.type === 'parens' && curToken.value === ')') {
          break;
          }
          // 參數(shù)間必須以 , 相間隔
          if (curToken.type !== 'comma' && curToken.value !== ',') {
          throw new Error('Expected , between arguments');
          }
          }
          }
          commit();
          return expr;
          }
          rewind();
          return identifier;
          }

          if (curToken.type === 'number' || curToken.type === 'string') {
          // 數(shù)字或字符串,說明此處是個常量表達式
          const literal = {
          type: 'Literal',
          value: eval(curToken.value),
          };
          // 但如果下一個符號是運算符,那么這就是個雙元運算表達式
          // 此處暫不考慮多個運算銜接,或者有變量存在
          stash();
          nextToken();
          if (curToken.type === 'operator') {
          commit();
          return {
          type: 'BinaryExpression',
          left: literal,
          right: nextExpression(),
          };
          }
          rewind();
          return literal;
          }

          if (curToken.type !== 'EOF') {
          throw new Error('Unexpected token ' + curToken.value);
          }
          }

          // 往后移動讀取指針,自動跳過空白
          function nextToken () {
          do {
          i++;
          curToken = tokens[i] || { type: 'EOF' };
          } while (curToken.type === 'whitespace');
          }

          // 位置暫存棧,用于支持很多時候需要返回到某個之前的位置
          const stashStack = [];

          function stash (cb) {
          // 暫存當前位置
          stashStack.push(i);
          }

          function rewind () {
          // 解析失敗,回到上一個暫存的位置
          i = stashStack.pop();
          curToken = tokens[i];
          }

          function commit () {
          // 解析成功,不需要再返回
          stashStack.pop();
          }

          const ast = {
          type: 'Program',
          body: [],
          };

          // 逐條解析頂層語句
          while (i < tokens.length) {
          const statement = nextStatement();
          if (!statement) {
          break;
          }
          ast.body.push(statement);
          }
          return ast;
          }

          const ast = parse([
          { type: "whitespace", value: "\n" },
          { type: "identifier", value: "if" },
          { type: "whitespace", value: " " },
          { type: "parens", value: "(" },
          { type: "number", value: "1" },
          { type: "whitespace", value: " " },
          { type: "operator", value: ">" },
          { type: "whitespace", value: " " },
          { type: "number", value: "0" },
          { type: "parens", value: ")" },
          { type: "whitespace", value: " " },
          { type: "brace", value: "{" },
          { type: "whitespace", value: "\n " },
          { type: "identifier", value: "alert" },
          { type: "parens", value: "(" },
          { type: "string", value: "\"if 1 > 0\"" },
          { type: "parens", value: ")" },
          { type: "sep", value: ";" },
          { type: "whitespace", value: "\n" },
          { type: "brace", value: "}" },
          { type: "whitespace", value: "\n" },
          ]);

          最終得到結果:

          {
          "type": "Program",
          "body": [
          {
          "type": "IfStatement",
          "test": {
          "type": "BinaryExpression",
          "left": {
          "type": "Literal",
          "value": 1
          },
          "right": {
          "type": "Literal",
          "value": 0
          }
          },
          "consequent": {
          "type": "BlockStatement",
          "body": [
          {
          "type": "ExpressionStatement",
          "expression": {
          "type": "CallExpression",
          "caller": {
          "type": "Identifier",
          "value": "alert"
          },
          "arguments": [
          {
          "type": "Literal",
          "value": "if 1 > 0"
          }
          ]
          }
          }
          ]
          },
          "alternative": null
          }
          ]
          }

          以上就是語義解析的部分主要思路。注意現(xiàn)在的nextExpression已經(jīng)頗為復雜,但實際實現(xiàn)要比現(xiàn)在這里展示的要更復雜很多,因為這里根本沒有考慮單元運算符、運算優(yōu)先級等等。

          結語

          真正看下來,其實沒有哪個地方的原理特別高深莫測,就是精細活,需要考慮到各種各樣的情況??傊鲆粋€完整的語法解釋器需要的是十分的細心與耐心。

          在并不是特別遠的過去,做web項目,前端技術都還很簡單,甚至那時候的網(wǎng)頁都盡量不用JavaScript。之后jQuery的誕生真正地讓JS成為了web應用開發(fā)核心,web前端工程師這種職業(yè)也才真正獨立出來。但后來隨著語言預處理和打包等技術的出現(xiàn),前端真的是越來越強大但是技術棧也真的是變得越來越復雜。雖然有種永遠都學不完的感覺,但這更能體現(xiàn)出我們前端工程存在的價值,不是嗎?

          - END -



          分享前端好文,點亮?在看?


          瀏覽 41
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  蜜乳一区二区三区免费 | 91成人视频18 | 超碰在线公开91 | 国产黄色三级片 | 在线能看的丝袜网站 |