AST笼统语法树——最基本的javascript重点学问,99%的人根本不相识

笼统语法树(AST),是一个异常基本而主要的知识点,但国内的文档却险些一片空白。

本文将带人人从底层相识AST,而且经由历程宣布一个小型前端东西,来带人人相识AST的壮大功用

Javascript就像一台精巧运作的机器,我们可以用它来完成统统天马行空的构想。

我们对javascript生态管窥蠡测,却常无视javascript自身。这台机器,究竟是哪些零部件在支持着它运转?

AST在一样平常营业中或许很难涉及到,但当你不止于想做一个工程师,而想做工程师的工程师,写出vue、react之类的大型框架,或相似webpack、vue-cli前端自动化的东西,或许有批量修正源码的工程需求,那你必需晓得AST。AST的才能非常壮大,且能帮你真正吃透javascript的言语精华。

事实上,在javascript天下中,你可以以为笼统语法树(AST)是最底层。 再往下,就是关于转换和编译的“黑魔法”范畴了。

人生第一次拆解Javascript

小时候,当我们拿到一个螺丝刀和一台机器,人生中最使人思念的梦境时候便最先了:

我们把机器,拆成一个一个小零件,一个个齿轮与螺钉,用奇妙的机器道理连接在一同…

当我们把它从新照差别的体式格局组装起来,这时候,机器从新又跑动了起来——天下在你眼中如获重生。

《AST笼统语法树——最基本的javascript重点学问,99%的人根本不相识》

经由历程笼统语法树剖析,我们可以像童年时拆解玩具一样,透视Javascript这台机器的运转,而且从新按着你的志愿来组装。

现在,我们拆解一个简朴的add函数

function add(a, b) {
    return a + b
}

起首,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象。

用力拆开,它成了三块:

  • 一个id,就是它的名字,即add
  • 两个params,就是它的参数,即[a, b]
  • 一块body,也就是大括号内的一堆东西

add没办法继承拆下去了,它是一个最基本Identifier(标志)对象,用来作为函数的唯一标志,就像人的姓名一样。

{
    name: 'add'
    type: 'identifier'
    ...
}

params继承拆下去,实际上是两个Identifier构成的数组。以后也没办法拆下去了。

[
    {
        name: 'a'
        type: 'identifier'
        ...
    },
    {
        name: 'b'
        type: 'identifier'
        ...
    }
]

接下来,我们继承拆开body
我们发明,body实际上是一个BlockStatement(块状域)对象,用来表示是{return a + b}

翻开Blockstatement,内里藏着一个ReturnStatement(Return域)对象,用来表示return a + b

继承翻开ReturnStatement,内里是一个BinaryExpression(二项式)对象,用来表示a + b

继承翻开BinaryExpression,它成了三部份,leftoperatorright

  • operator+
  • left 内里装的,是Identifier对象 a
  • right 内里装的,是Identifer对象 b

就如许,我们把一个简朴的add函数拆解终了,用图表示就是

《AST笼统语法树——最基本的javascript重点学问,99%的人根本不相识》

看!笼统语法树(Abstract Syntax Tree),的确是一种规范的树构造。

那末,上面我们提到的Identifier、Blockstatement、ReturnStatement、BinaryExpression, 这一个个小部件的说明书去哪查?

请检察 AST对象文档

送给你的AST螺丝刀:recast

输入敕令:

npm i recast -S

你即可取得一把支配语法树的螺丝刀

接下来,你可以在恣意js文件下支配这把螺丝刀,我们新建一个parse.js表示:

parse.js

// 给你一把"螺丝刀"——recast
const recast = require("recast");

// 你的"机器"——一段代码
// 我们运用了很新鲜花样的代码,想测试是不是能保持代码构造
const code =
  `
  function add(a, b) {
    return a +
      // 有什么新鲜的东西混进来了
      b
  }
  `
// 用螺丝刀剖析机器
const ast = recast.parse(code);

// ast可以处置惩罚很庞大的代码文件
// 但我们现在只须要代码块的第一个body,即add函数
const add  = ast.program.body[0]

console.log(add)

输入node parse.js你可以检察到add函数的构造,与之前所述一致,经由历程AST对象文档可查到它的详细属性:

FunctionDeclaration{
    type: 'FunctionDeclaration',
    id: ...
    params: ...
    body: ...
}

你也可以继承运用console.log透视它的更内层,如:

console.log(add.params[0])
console.log(add.body.body[0].argument.left)

recast.types.builders 制造模具

一个机器,你只会拆开重装,不算本领。

拆开了,还能改装,才算上得了台面。

recast.types.builders内里供应了不少“模具”,让你可以轻松地拼接成新的机器。

最简朴的例子,我们想把之前的function add(a, b){...}声明,改成匿名函数式声明const add = function(a ,b){...}

怎样改装?

第一步,我们建立一个VariableDeclaration变量声明对象,声明头为const, 内容为一个行将建立的VariableDeclarator对象。

第二步,建立一个VariableDeclarator,安排add.id在左侧, 右侧是将建立的FunctionDeclaration对象

第三步,我们建立一个FunctionDeclaration,如前所述的三个组件,id params body中,由于是匿名函数id设为空,params运用add.params,body运用add.body。

如许,就建立好了const add = function(){}的AST对象。

在之前的parse.js代码以后,到场以下代码

// 引入变量声明,变量标记,函数声明三种“模具”
const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders

// 将准备好的组件置入模具,并组装回本来的ast对象。
ast.program.body[0] = variableDeclaration("const", [
  variableDeclarator(add.id, functionExpression(
    null, // Anonymize the function expression.
    add.params,
    add.body
  ))
]);

//将AST对象从新转回可以浏览的代码
const output = recast.print(ast).code;

console.log(output)

可以看到,我们打印出了

const add = function(a, b) {
  return a +
    // 有什么新鲜的东西混进来了
    b
};

末了一行

const output = recast.print(ast).code;

实际上是recast.parse的逆向历程,详细公式为

recast.print(recast.parse(source)).code === source

打印出来还保留着“原装”的函数内容,连解释都没有变。

我们实在也可以打印出美化花样的代码段:

const output = recast.prettyPrint(ast, { tabWidth: 2 }).code

输出为

const add = function(a, b) {
  return a + b;
};

现在,你是不是是已产生了“我可以经由历程AST树天生任何js代码”的幻觉?

我慎重通知你,这不是幻觉。

实战进阶:敕令行修正js文件

除了parse/print/builder之外,Recast的三项主要功用:

  • run: 经由历程敕令行读取js文件,并转化成ast以供处置惩罚。
  • tnt: 经由历程assert()和check(),可以考证ast对象的范例。
  • visit: 遍历ast树,猎取有用的AST对象并举行变动。

我们经由历程一个系列小务来进修悉数的recast东西库:

建立一个用来示例文件,假设是demo.js

demo.js

function add(a, b) {
  return a + b
}

function sub(a, b) {
  return a - b
}

function commonDivision(a, b) {
  while (b !== 0) {
    if (a > b) {
      a = sub(a, b)
    } else {
      b = sub(b, a)
    }
  }
  return a
}

recast.run —— 敕令行文件读取

新建一个名为read.js的文件,写入
read.js

recast.run( function(ast, printSource){
    printSource(ast)
})

敕令行输入

node read demo.js

我们查以看到js文件内容打印在了掌握台上。

我们可以晓得,node read可以读取demo.js文件,并将demo.js内容转化为ast对象。

同时它还供应了一个printSource函数,随时可以将ast的内容转换回源码,以轻易调试。

recast.visit —— AST节点遍历

read.js

#!/usr/bin/env node
const recast  = require('recast')

recast.run(function(ast, printSource) {
  recast.visit(ast, {
      visitExpressionStatement: function({node}) {
        console.log(node)
        return false
      }
    });
});

recast.visit将AST对象内的节点举行逐一遍历。

注重

  • 你想操纵函数声明,就运用visitFunctionDelaration遍历,想操纵赋值表达式,就运用visitExpressionStatement。 只需在 AST对象文档中定义的对象,在前面加visit,即可遍历。
  • 经由历程node可以取到AST对象
  • 每一个遍历函数后必需加上return false,或许挑选以下写法,不然报错:
#!/usr/bin/env node
const recast  = require('recast')

recast.run(function(ast, printSource) {
  recast.visit(ast, {
      visitExpressionStatement: function(path) {
        const node = path.node
        printSource(node)
        this.traverse(path)
      }
    })
});

调试时,假如你想输出AST对象,可以console.log(node)

假如你想输出AST对象对应的源码,可以printSource(node)

敕令行输入`
node read demo.js`举行测试。

#!/usr/bin/env node 在一切运用
recast.run()的文件顶部都须要到场这一行,它的意义我们末了再议论。

TNT —— 推断AST对象范例

TNT,即recast.types.namedTypes,就像它的名字一样火爆,它用来推断AST对象是不是为指定的范例。

TNT.Node.assert(),就像在机器里埋好的火药,当机器不能无缺运转时(范例不婚配),就炸毁机器(报错退出)

TNT.Node.check(),则可以推断范例是不是一致,并输出False和True

上述Node可以替代成恣意AST对象,比方TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert()

read.js

#!/usr/bin/env node
const recast = require("recast");
const TNT = recast.types.namedTypes

recast.run(function(ast, printSource) {
  recast.visit(ast, {
      visitExpressionStatement: function(path) {
        const node = path.value
        // 推断是不是为ExpressionStatement,准确则输出一行字。
        if(TNT.ExpressionStatement.check(node)){
          console.log('这是一个ExpressionStatement')
        }
        this.traverse(path);
      }
    });
});

read.js

#!/usr/bin/env node
const recast = require("recast");
const TNT = recast.types.namedTypes

recast.run(function(ast, printSource) {
  recast.visit(ast, {
      visitExpressionStatement: function(path) {
        const node = path.node
        // 推断是不是为ExpressionStatement,准确不输出,毛病则全局报错
        TNT.ExpressionStatement.assert(node)
        this.traverse(path);
      }
    });
});

敕令行输入`
node read demo.js`举行测试。

实战:用AST修正源码,导出悉数要领

exportific.js

现在,我们想让这个文件中的函数改写成可以悉数导出的情势,比方

function add (a, b) {
    return a + b
}

想改变成

exports.add = (a, b) => {
  return a + b
}

除了运用fs.read读取文件、正则婚配替代文本、fs.write写入文件这类愚笨的体式格局外,我们可以用AST文雅地解决问题

查询AST对象文档

起首,我们先用builders平空完成一个键头函数

exportific.js

#!/usr/bin/env node
const recast = require("recast");
const {
  identifier:id,
  expressionStatement,
  memberExpression,
  assignmentExpression,
  arrowFunctionExpression,
  blockStatement
} = recast.types.builders

recast.run(function(ast, printSource) {
  // 一个块级域 {}
  console.log('\n\nstep1:')
  printSource(blockStatement([]))

  // 一个键头函数 ()=>{}
  console.log('\n\nstep2:')
  printSource(arrowFunctionExpression([],blockStatement([])))

  // add赋值为键头函数  add = ()=>{}
  console.log('\n\nstep3:')
  printSource(assignmentExpression('=',id('add'),arrowFunctionExpression([],blockStatement([]))))

  // exports.add赋值为键头函数  exports.add = ()=>{}
  console.log('\n\nstep4:')
  printSource(expressionStatement(assignmentExpression('=',memberExpression(id('exports'),id('add')),
    arrowFunctionExpression([],blockStatement([])))))
});

上面写了我们一步一步推断出exports.add = ()=>{}的历程,从而获得详细的AST构造体。

运用node exportific demo.js运转可检察效果。

接下来,只须要在取得的终究的表达式中,把id(‘add’)替代成遍历获得的函数名,把参数替代成遍历获得的函数参数,把blockStatement([])替代为遍历获得的函数块级作用域,就成功地改写了一切函数!

别的,我们须要注重,在commonDivision函数内,引用了sub函数,应改写成exports.sub

exportific.js

#!/usr/bin/env node
const recast = require("recast");
const {
  identifier: id,
  expressionStatement,
  memberExpression,
  assignmentExpression,
  arrowFunctionExpression
} = recast.types.builders

recast.run(function (ast, printSource) {
  // 用来保留遍历到的悉数函数名
  let funcIds = []
  recast.types.visit(ast, {
    // 遍历一切的函数定义
    visitFunctionDeclaration(path) {
      //猎取遍历到的函数名、参数、块级域
      const node = path.node
      const funcName = node.id
      const params = node.params
      const body = node.body

      // 保留函数名
      funcIds.push(funcName.name)
      // 这是上一步推导出来的ast构造体
      const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
        arrowFunctionExpression(params, body)))
      // 将本来函数的ast构造体,替代成推导ast构造体
      path.replace(rep)
      // 住手遍历
      return false
    }
  })


  recast.types.visit(ast, {
    // 遍历一切的函数挪用
    visitCallExpression(path){
      const node = path.node;
      // 假如函数挪用出现在函数定义中,则修正ast构造
      if (funcIds.includes(node.callee.name)) {
        node.callee = memberExpression(id('exports'), node.callee)
      }
      // 住手遍历
      return false
    }
  })
  // 打印修正后的ast源码
  printSource(ast)
})

一步到位,发一个最简朴的exportific前端东西

上面讲了那末多,依然只体现在理论阶段。

但经由历程简朴的改写,就可以经由历程recast制造成一个名为exportific的源码编辑东西。

以下代码增加作了两个小修改

  1. 增加说明书–help,以及增加了–rewrite形式,可以直接掩盖文件或默以为导出*.export.js文件。
  2. 将之前代码末了的 printSource(ast)替代成 writeASTFile(ast,filename,rewriteMode)

exportific.js

#!/usr/bin/env node
const recast = require("recast");
const {
  identifier: id,
  expressionStatement,
  memberExpression,
  assignmentExpression,
  arrowFunctionExpression
} = recast.types.builders

const fs = require('fs')
const path = require('path')
// 截取参数
const options = process.argv.slice(2)

//假如没有参数,或供应了-h 或--help选项,则打印协助
if(options.length===0 || options.includes('-h') || options.includes('--help')){
  console.log(`
    采纳commonjs划定规矩,将.js文件内一切函数修正为导出情势。

    选项: -r  或 --rewrite 可直接掩盖原有文件
    `)
  process.exit(0)
}

// 只需有-r 或--rewrite参数,则rewriteMode为true
let rewriteMode = options.includes('-r') || options.includes('--rewrite')

// 猎取文件名
const clearFileArg = options.filter((item)=>{
  return !['-r','--rewrite','-h','--help'].includes(item)
})

// 只处置惩罚一个文件
let filename = clearFileArg[0]

const writeASTFile = function(ast, filename, rewriteMode){
  const newCode = recast.print(ast).code
  if(!rewriteMode){
    // 非掩盖形式下,将新文件写入*.export.js下
    filename = filename.split('.').slice(0,-1).concat(['export','js']).join('.')
  }
  // 将新代码写入文件
  fs.writeFileSync(path.join(process.cwd(),filename),newCode)
}


recast.run(function (ast, printSource) {
  let funcIds = []
  recast.types.visit(ast, {
    visitFunctionDeclaration(path) {
      //猎取遍历到的函数名、参数、块级域
      const node = path.node
      const funcName = node.id
      const params = node.params
      const body = node.body

      funcIds.push(funcName.name)
      const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
        arrowFunctionExpression(params, body)))
      path.replace(rep)
      return false
    }
  })


  recast.types.visit(ast, {
    visitCallExpression(path){
      const node = path.node;
      if (funcIds.includes(node.callee.name)) {
        node.callee = memberExpression(id('exports'), node.callee)
      }
      return false
    }
  })

  writeASTFile(ast,filename,rewriteMode)
})

现在尝试一下

node exportific demo.js

已可以在当前目录下找到源码变动后的demo.export.js文件了。

npm发包

编辑一下package.json文件

{
  "name": "exportific",
  "version": "0.0.1",
  "description": "改写源码中的函数为可exports.XXX情势",
  "main": "exportific.js",
  "bin": {
    "exportific": "./exportific.js"
  },
  "keywords": [],
  "author": "wanthering",
  "license": "ISC",
  "dependencies": {
    "recast": "^0.15.3"
  }
}

注重bin选项,它的意义是将全局敕令exportific指向当前目录下的exportific.js

这时候,输入npm link 就在当地天生了一个exportific敕令。

以后,只需哪一个js文件想导出来运用,就exportific XXX.js一下。

这是在当地的弄法,想和人人一同分享这个前端小东西,只须要宣布npm包就好了。

同时,肯定要注重exportific.js文件头有

#!/usr/bin/env node

不然在运用时将报错。

接下来,正式宣布npm包!

假如你已有了npm 帐号,请运用npm login登录

假如你还没有npm帐号 https://www.npmjs.com/signup 异常简朴就可以注册npm

然后,输入
npm publish

没有任何烦琐步骤,涓滴考核都没有,你就宣布了一个有用的前端小东西exportific 。任何人都可以经由历程

npm i exportific -g

全局装置这一个插件。

提醒:==在实验教程时,请不要和我的包重名,修正一下发包称号。==

结语

我们对javascript再熟习不过,但透过AST的视角,最一般的js语句,却焕发出精心动魄的美感。你可以经由历程它批量构建任何javascript代码!

童年时,这个天下充满了新颖的玩具,再一般的东西在你眼中都犹如珍宝。现在,计算机言语就是你手中的大玩具,一段段AST对象的拆分组装,构建出我们所生涯的网络天下。

所以不得不说软件工程师是一个幸运的事情,你心中住的依然是谁人午后的少年,永久有无数新颖等你发明,永久有无数妄想等你构建。

github地点:https://github.com/wanthering…

《AST笼统语法树——最基本的javascript重点学问,99%的人根本不相识》

    原文作者:刘羽冲
    原文地址: https://segmentfault.com/a/1190000016231512
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞