從babel講到AST

前言

最近給團隊分享了一篇babel原理,而後我把他整理成一篇blog,本篇總字數6059(含代碼),速讀3分鐘,普通閱讀5分鐘,有興趣的能夠關注一下個人github博客webpack

babel

咱們來看一段代碼:git

[1,2,3].map(n => n + 1);
複製代碼

通過babel以後,這段代碼變成了這樣:github

[1, 2, 3].map(function (n) {
  return n + 1;
});
複製代碼

babel的背後

babel的過程:解析——轉換——生成。web

這邊又一箇中間的東西,是抽象語法樹(AST)express

AST的解析過程

一個js語句是怎麼被解析成AST的呢?這個中間有兩個步驟,一個是分詞,第二個是語義分析,怎麼理解這兩個東西呢?數組

  • 分詞

什麼叫分詞?babel

好比咱們在讀一句話的時候,咱們也會作分詞操做,好比:「今每天氣真好」,咱們會把他切割成「今天」,「天氣」,「真好」。ide

那換成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

這篇文章講完了,其實不理解代碼不要緊,把總體思路把握住就行。

相關文章
相關標籤/搜索