likes
comments
collection
share

浅析AST

作者站长头像
站长
· 阅读数 79

什么是Ast(Abstract Syntax Tree)

抽象语法树(Abstract Syntax Tree)简称是AST。顾名思义就是将源代码的语法结构抽象成树状表现形式,树上的每一个节点都对应着源码中的一部分。所谓的"树"就是把我们的js语句转化为一个json对象,有根节点、分枝、树叶像是一棵完整的树。

我们可以通过astexplorer从线上观察AST的转化结果。

浅析AST

那为什么要把我们看起来好懂的js代码,转化为一个抽象的树呢?

其实对于javaScript引擎来说,做的第一件事就是把我们的js代码转化为抽象语法树,之后才是转换为字节码供解释器使用。虽然对于我们程序员来说,js代码更容易理解,但是对于计算机来说,这个抽象的树才是更容易理解的。

AST的用途

AST的使用其实还是相当广泛的,即使你没有听说过AST,但是babel``eslint``prettier你肯定是接触过的,而这些工具,他们对静态代码进行翻译、代码检查、格式化等等功能实现都是通过AST。

AST解析流程

AST是深度优先的遍历,常见的jsParser有estree、esprima、acorn、babel parser,这里我们通过esprima简单的了解一下流程。

安装如下npm包

npm i esprima estraverse escodegen --save
  • esprima:将JS源代码转化为AST语法树。
  • estraverse:遍历语法树,修改树上的节点。
  • escodegen:把AST语法树重新转化为代码。
const esprima = require('esprima'); //把JS源代码转成AST语法树
const estraverse = require('estraverse'); ///遍历语法树,修改树上的节点
let escodegen = require('escodegen'); //把AST语法树重新转换成代码

const code = `function ast(){}`;
const ast = esprima.parse(code);

estraverse.traverse(ast, {
  enter(node) {
    console.log(node.type + '进入');
    if (node.type === 'FunctionDeclaration') {
      node.id.name = 'newAst';
    }
  },
  leave(node) {
    console.log(node.type + '离开');
  },
});

const newCode = escodegen.generate(ast);

console.log(newCode);

输出结果如下:

Program进入
FunctionDeclaration进入
Identifier进入
Identifier离开
BlockStatement进入
BlockStatement离开
FunctionDeclaration离开
Program离开
function newAst() {
}

我们成功的生成了code的语法树,成功遍历并修改函数名称,将ast => newAst。

babel中的AST

我们都知道babel可以将Es6代码转换为Es5代码,来解决一些浏览器不兼容Es6新特性的问题,babel能完成这项工作自然是离不开AST。

工作过程分为三个部分:

  • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
  • Transform(转换) 对抽象语法树进行转换
  • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

实现一个babel插件

visitor

  • 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
  • Visitor 的对象定义了用于 AST 中获取具体节点的方法
  • Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法

体验官方插件

在这里实现一个箭头函数转换为普通函数的插件,这里我选择使用的是babel中@babel/core和@babel/types实现,这里的babel-plugin-transform-es2015-arrow-functions是官方插件,代码如下:

npm install @babel/core @babel/core @babel/types babel-plugin-transform-es2015-arrow-functions --save-dev
const core = require('@babel/core');
const types = require('@babel/types');
const arrowFunctionPlugin = require('babel-plugin-transform-es2015-arrow-functions');

const sourceCode = `
const sum = (a, b) => {
  return a+b;
}
`;

const result = core.transform(sourceCode, {
  plugins: [arrowFunctionPlugin],
});

console.log(result); 
// 输出结果如下
/*
const sum = function (a, b) {
  return a + b;
};
*/

实现插件

下面我们实现我们自己的插件

首先我们通过astexplorer观察一下箭头函数和普通函数在ast上表现的不同:

浅析AST

浅析AST

看起来只是有图中圈中的位置不同我们尝试改变一下:

const core = require('@babel/core');
const types = require('@babel/types');

const arrowFunctionPlugin = {
  visitor: {
    ArrowFunctionExpression(path) {
      const { node } = path;

      node.type = 'FunctionExpression';
    },
  },
};

const sourceCode = `
const sum = (a, b) => {
  return a+b;
}
`;

const result = core.transform(sourceCode, {
  plugins: [arrowFunctionPlugin],
});

console.log(result.code);

// 输出结果
/*
const sum = function (a, b) {
  return a + b;
};
*/

amazing!🎉🎉🎉 看起来我们已经完成了这个插件,也不过如此。事实上这只是完成了其中的一部分,我们还有一些情况是没有考虑的,这些情况这个插件就会显得能力不足,比如:

// 简写return的情况
const sourceCode = `
const sum = (a, b) => a+b;
`;

// 转化为
const sum = function (a, b) a + b;

而这显然是一个错误的答案,这时我们需要对我们插件进行一些改造。

对比简写和非简写ast的异同

浅析AST

浅析AST

根据对比我们可以发现,非简写比简写的多出来一层BlockStatement节点,也就是我们的{},我们可以根据这个不同来丰富我们的插件

const arrowFunctionPlugin = {
  visitor: {
    ArrowFunctionExpression(path) {
      const { node } = path;

      node.type = 'FunctionExpression';

      const body = node.body;
      // 判断是不是一个blockStatement,这里是兼容简写的模式
      if (!types.isBlockStatement(body)) {
        // 快速方便的构建节点
        node.body = types.blockStatement([types.returnStatement(body)]);
      }
    },
  },
};

通过丰富的插件我们可以得到转换为:

// 简写return的情况
const sourceCode = `
const sum = (a, b) => a+b;
`;

// 转化为
const sum = function (a, b) {
  return a + b;
};

看来我们距离成功又近了一步。为什么这么说,是因为我们还有最为关键的一步没有实现,就是箭头函数中this指向的问题。我们都知道,箭头函数没有自己的this,他所依赖的是他外层的this。

首先我们看一下,官方插件是怎么处理的。

const sourceCode = `
const sum = (a, b) => {
  console.log(this)
  return a+b;
}
`;

// 转化为
var _this = this;

const sum = function (a, b) {
  cconsole.log(_this);
  return a + b;
};

它先在函数外层创建了一个_this变量,并把当前上下文中的this赋值给它,之后在函数中引用我们的_this变量即可,这样我们也仿造这么写一个。

  • 首先,我们检测在箭头函数中有没有使用到this,
function getThisPaths(path) {
  const thisPaths = [];

  // 遍历传进来的函数,通过traverse方法将使用到this的节点保存
  path.traverse({
    ThisExpression(thisPath) {
      thisPaths.push(thisPath);
    },
  });

  return thisPaths;
}
  • 如果存在使用this的节点,即thisPaths的节点的长度是大于0的,就寻找父节点,找到真正的this,这里要保证父节点不能是箭头函数,或者到了根节点就停止。
 // 1. 检测当前节点有没有用到this
  const thisPaths = getThisPaths(path);

  if (thisPaths.length > 0) {
    // 2. 寻找外层this的路径,这里要寻找父节点,直到外层是函数且不是箭头函数,或者到了根节点,来确定this的路径
    const thisEnv = path.findParent((parent) => {
      // 如果是函数,但是不是箭头函数,或者是根节点,就返回true
      return (
        (parent.isFunction() && !parent.isArrowFunctionExpress()) ||
        parent.isProgram()
      );
    });
  }
  • 最后一步,创建_this变量,保证该路径没有添加过该变量之后,添加该变量,避免重复添加。
const thisBindings = '_this';
    // 3. 如果路径中的作用域中没有绑定过_this,就向作用域中增加一个,避免重复绑定
    if (!thisEnv.scope.hasBinding(thisBindings)) {
      // 向对应的作用域中添加一个_this,他的值就是this
      const thisIdentifier = types.identifier(thisBindings);
      thisEnv.scope.push({
        id: thisIdentifier,
        init: types.thisExpression(),
      });

      // 遍历添加
      thisPaths.forEach((thisPath) => {
        thisPath.replaceWith(thisIdentifier);
      });
    }

封装成一个方法

function hoistFunctionEnvironment(path) {
  // 1. 检测当前节点有没有用到this
  const thisPaths = getThisPaths(path);

  if (thisPaths.length > 0) {
    // 2. 寻找外层this的路径,这里要寻找父节点,直到外层是函数且不是箭头函数,或者到了根节点,来确定this的路径
    const thisEnv = path.findParent((parent) => {
      // 如果是函数,但是不是箭头函数,或者是根节点,就返回true
      return (
        (parent.isFunction() && !parent.isArrowFunctionExpress()) ||
        parent.isProgram()
      );
    });

    const thisBindings = '_this';
    // 3. 如果路径中的作用域中没有绑定过_this,就向作用域中增加一个,避免重复绑定
    if (!thisEnv.scope.hasBinding(thisBindings)) {
      // 向对应的作用域中添加一个_this,他的值就是this
      const thisIdentifier = types.identifier(thisBindings);
      thisEnv.scope.push({
        id: thisIdentifier,
        init: types.thisExpression(),
      });

      // 遍历添加
      thisPaths.forEach((thisPath) => {
        thisPath.replaceWith(thisIdentifier);
      });
    }
  }
}

讲上述函数插入到合适的位置

const arrowFunctionPlugin = {
  visitor: {
    ArrowFunctionExpression(path) {
      const { node } = path;

      node.type = 'FunctionExpression';

      // 判断this
      hoistFunctionEnvironment(path);

      const body = node.body;
      // 判断是不是一个blockStatement,这里是想兼容简写的模式
      if (!types.isBlockStatement(body)) {
        // 快速方便的构建节点
        node.body = types.blockStatement([types.returnStatement(body)]);
      }
    },
  },
};

到这里我们的插件就基本上算是完成了,体验一下

// 转化前
const sourceCode = `
const sum = (a, b) => {
  cconsole.log(this)
  return a+b;
}
`;

// 转化后
var _this = this;

const sum = function (a, b) {
  cconsole.log(_this);
  return a + b;
};

🎉🎉🎉 这就与官方插件一致了。

完整代码

const core = require('@babel/core');
const types = require('@babel/types');

const arrowFunctionPlugin = {
  visitor: {
    ArrowFunctionExpression(path) {
      const { node } = path;

      node.type = 'FunctionExpression';

      // 判断this
      hoistFunctionEnvironment(path);

      const body = node.body;
      // 判断是不是一个blockStatement,这里是想兼容简写的模式
      if (!types.isBlockStatement(body)) {
        // 快速方便的构建节点
        node.body = types.blockStatement([types.returnStatement(body)]);
      }
    },
  },
};

function hoistFunctionEnvironment(path) {
  // 1. 检测当前节点有没有用到this
  const thisPaths = getThisPaths(path);

  if (thisPaths.length > 0) {
    // 2. 寻找外层this的路径,这里要寻找父节点,直到外层是函数且不是箭头函数,或者到了根节点,来确定this的路径
    const thisEnv = path.findParent((parent) => {
      // 如果是函数,但是不是箭头函数,或者是根节点,就返回true
      return (
        (parent.isFunction() && !parent.isArrowFunctionExpress()) ||
        parent.isProgram()
      );
    });

    const thisBindings = '_this';
    // 3. 如果路径中的作用域中没有绑定过_this,就向作用域中增加一个,避免重复绑定
    if (!thisEnv.scope.hasBinding(thisBindings)) {
      // 向对应的作用域中添加一个_this,他的值就是this
      const thisIdentifier = types.identifier(thisBindings);
      thisEnv.scope.push({
        id: thisIdentifier,
        init: types.thisExpression(),
      });

      // 遍历添加
      thisPaths.forEach((thisPath) => {
        thisPath.replaceWith(thisIdentifier);
      });
    }
  }
}
function getThisPaths(path) {
  const thisPaths = [];

  path.traverse({
    ThisExpression(thisPath) {
      thisPaths.push(thisPath);
    },
  });

  return thisPaths;
}

const sourceCode = `
const sum = (a, b) => {
  cconsole.log(this)
  return a+b;
}
`;

const result = core.transform(sourceCode, {
  plugins: [arrowFunctionPlugin],
});

console.log(result.code);
转载自:https://juejin.cn/post/7179577515663589435
评论
请登录