从 babel 原理讲到 AST

发布于 2023-03-18 21:39:40 字数 9891 浏览 2 评论 0

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语句去判断,然后最后结果存入一个数组中,对于标识符和数字进行特殊处理functiontokenCode(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;
          }
          thrownewError('Unexpected ' + currentChar);
        }
    return tokens;
}

语义分析

语义分析的话就比较难了,为什么这么说呢?因为这个不像分词这样有个标准,有些东西都要靠自己去摸索。其实语义分析分为两块,一块是语句,还有一块是表达式。

什么叫语句?什么叫表达式呢?

表达式,比如:a > b; a + b; 这一类的,可以嵌套,也可以运用在语句中。语句,比如:var a = 1, b = 2, c = 3 等,我们理解中的一个语句。类似于语文中的一个句子一样。当然,有人会问,console.log(1); 这个算什么呢。

其实这种情况可以归为一类,单语句表达式,你既可以看作表达式,也可以看作语句,一个表达式单成一个语句。既然分完了,我们也可以尝试这来写一下,简单点的语句分析。比如var定义语句,或者复杂点的 if 语句块。

生成 AST 的形式可以参考这个网站,AST 的一些语法可以从这个网站试出个大概

//思路分析:既然分三种情况,那么我们也从语句,表达式,单语句表达式入手,我们先定义一个方法用来分析表达式,在定义一个方法来分析语句,最后在定义一个方法分析单语句表达式。整个过程也是分为那么几步。就多了对于指针的管控。functionparse (tokens) {
    // 位置暂存栈,用于支持很多时候需要返回到某个之前的位置const stashStack = [];
    let i = -1;     // 用于标识当前遍历位置let curToken;   // 用于记录当前符号// 暂存当前位置  functionstash () {
        stashStack.push(i);
    }
      // 往后移动读取指针functionnextToken () {
        i++;
        curToken = tokens[i] || { type: 'EOF' };;
    }

    functionparseFalse () {
      // 解析失败,回到上一个暂存的位置
      i = stashStack.pop();
      curToken = tokens[i];
    }

    functionparseSuccess () {
      // 解析成功,不需要再返回
      stashStack.pop();
    }
  
    const ast = {
        type: 'Program',
        body: [],
        sourceType: "script"
    };

  // 读取下一个语句functionnextStatement () {
    // 暂存当前的i,如果无法找到符合条件的情况会需要回到这里
    stash();
    
    // 读取下一个符号
    nextToken();

    if (curToken.type === 'identifier' && curToken.value === 'if') {
      // 解析 if 语句const statement = {
        type: 'IfStatement',
      };
      // if 后面必须紧跟着 (
      nextToken();
      if (curToken.type !== 'Punctuator' || curToken.value !== '(') {
        thrownewError('Expected ( after if');
      }

      // 后续的一个表达式是 if 的判断条件
      statement.test = nextExpression();

      // 判断条件之后必须是 )
      nextToken();
      if (curToken.type !== 'Punctuator' || curToken.value !== ')') {
        thrownewError('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;
    }
  }

  // 读取下一个表达式functionnextExpression () {
    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();
          thrownewError('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') {
      thrownewError('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

这篇文章讲完了,其实不理解代码没关系,把整体思路把握住就行。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

列表为空,暂无数据

关于作者

山色无中

暂无简介

0 文章
0 评论
1 人气
更多

推荐作者

作业与我同在

文章 0 评论 0

github_mZrHPYV6X5

文章 0 评论 0

浪漫之都

文章 0 评论 0

享受孤独

文章 0 评论 0

最好是你

文章 0 评论 0

苏璃陌

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击“接受”或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文