AST 基础

AST 的基本结构

JS 代码解析成 AST 以后,类似于中间形式的 JSON 数据
经过 Babel 解析以后,通常将里面的元素叫做节点(Node)
  - Babel 提供了很多方法去操作这些节点

demo.js

let num1 = 1;
let num2 = 2;

AST explorer
在这里插入图片描述

body 中的元素,被称之为节点(Node)
在这里插入图片描述

每个节点都有自己对应的属性名,属性值
在这里插入图片描述

安装 babel 库

npn install @babel/core

// file_name: demo.js
let num1 = 1;
let num2 = 2;
// file_name: AST.js
// 安装 babel 库:  npm install @babel/core
const fs = require('fs')

const traverse = require('@babel/traverse').default  // 用于遍历 AST 节点
const types = require('@babel/types')  // 用于判断, 生成 AST 节点

const parser = require('@babel/parser')  // 将js代码解析成ast节点
const generator = require('@babel/generator').default  // 将ast节点转换成js代码

// 读取
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})

let ast = parser.parse(ast_code)  // 将js代码解析成ast语法树
// 在这里对 ast 进行一系列操作
traverse(ast,{
    VariableDeclaration(path){
        console.log(path + '')  // 打印当前遍历到的节点
    }
})



let js_code = generator(ast).code  // 将ast节点转换成js代码

// 写入
fs.writeFileSync('New_demo.js', js_code, {
    encoding: 'utf-8'
})

在这里插入图片描述

babel 中的组件

parse 与 generator

parse

parse 用来将 JS 代码转换成 AST 节点

// 节点的信息一般都是通过网站查看的 https://astexplorer.net/
// 网站上使用的 parser 需要和代码中的 parser 匹配,结构才能一样
let ast = parser.parse(ast_code, {
    // 当解析的代码中有 import 字眼的时候,需要指定 sourceType 为true
    // 这种情况很少见(很少使用)
    sourceType: true,
})  // 将 js代码 转换成 ast

generator

generator 用来将 AST节点 转换成 JS代码

// generator 实际上是一个对象,对象中code属性就是转换好的js代码(最常使用的方式)
let js_code = generator(ast, {
    retainLines: false,  // 是否使用与源代码相同的行号,默认false
    comments: true,  // 是否保留注释,默认true
    compact: false,  // 是否压缩,默认 false
}).code  // 将 ast 转换成 js代码

完整代码

// file_name: demo.js
let num1 = 1;
let num2 = 2;
// AST.js
const fs = require('fs')
const parser = require('@babel/parser')  // 将js代码解析成ast节点
const generator = require('@babel/generator').default  // 将ast节点转换成js代码

// 读取
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})

let ast = parser.parse(ast_code)  // 将js代码解析成ast语法树
let js_code = generator(ast).code  // 将ast节点转换成js代码
console.log(js_code)

traverse 与 visitor

traverse

traverse 用来遍历 ast 节点,简单地说就是把 ast 上的各个节点都走一遍.

单纯的把节点都走一遍是没有意义的,所以 traverse 需要配合 visitor 使用

// file_name: demo.js
let Obj = {
    name: 'xiaojiangbang',
    add: function(){
        return a + b + 1000;
    },
    mul: function (a, b) {
        return a * b + 1000;
    }
}

将代码中的 标识符a 修改成 x
在这里插入图片描述

// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const parser = require('@babel/parser')
const generator = require('@babel/generator').default

// 读取
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})

let ast = parser.parse(ast_code)
let visitor = {
    // 遍历语法树中所有的 Identifier 节点
    Identifier: function(path){
        // 传进来的参数是path对象
        // 通过path.node可以得到当前的节点
        // path.node.name 可以得到标识符对应的名字
        console.log(path.node.name);
        // path.parentPath.toString() 可以得到当前节点的父节点对应的js代码
        console.log(path.parentPath.toString())
        console.log('==============================')
        if (path.node.name === 'a'){  // 判断
            path.node.name = 'x';  // 修改
        }
    }
}
// 将代码中的标识符 a 修改成 x
traverse(ast,visitor)



let js_code = generator(ast).code

// 写入
fs.writeFileSync('New_demo.js', js_code, {
    encoding: 'utf-8'
})

运行后的代码

在这里插入图片描述

visitor 的定义方式

定义方式(常用的定义方式 visitor2 / visitor3

// 最常用的是 visitor1 / visitor2
// ES6 之前的语法
const visitor1 = {
    Identifier: function(path){
        console.log(path.node.name)
    }
}


// ES6 的语法
const visitor2 = {
    Identifier(path){
        console.log(path.node.name)
    }
}

// ES6 的语法
// 深度优先的遍历方式
// visitor3 中存在一个重要的 enter,在遍历节点过程中,实际上有两次机会来访问一个节点
// 进入节点时(enter) 退出节点时(exit)
// 可以只选择一个,指明是进入节点还是退出节点时
const visitor3 = {
    Identifier:{
        enter(path){
            // 进入节点时
            console.log('enter: ', path.node.name)
        },
        exit(path){
            // 退出退出节点时
            console.log('exit: ', path.node.name)
        }
    }
}
// 如果存在父子节点,
// enter 的处理时机是先处理父节点,再处理子节点
// exit 的处理时机是先处理子节点,再处理父节点
// traverse 默认就是在 enter 时候处理,如果要在 exit 时候处理,必须在 visitor 中指明

把同一个函数应用到多个节点(不常用)

const visitor = {
    // 同时遍历3个节点,不管碰到哪个都会进入函数执行 console.log(path.node.type)
    // 如果同时有更多个节点需要处理的,则继续在 | 后面加上类型
    "FunctionExpression|BinaryExpression|Identifier": function(path){
        console.log(path.node.type)
    }
}

将多个函数应用于同一个节点(不常用)

function fuc1(path){
    console.log('fuc1: ', path.node.name)
}
function fuc2(path){
    console.log('fuc2: ', path.node.type)
}

const visitor = {
    Identifier: {
        enter: [fuc1, fuc2]  // 相当于一个节点执行2个函数(不限函数数量),从左到右依次执行
    }
}

path 对象中的 traverse

traverse 是将传入的 ast 节点从头跑一遍(全局遍历)(最常用的方式)
path 对象中的 traverse 是从 traverse 遍历到的节点中再继续向下遍历

file_name: demo.js
let a = 100;
let obj = {
    name: 'xiaojianbang',
    add: function(a, b){
        return a + b + 1000;
    },
    mul: function (a, b){
        return a * b + 1000;
    }
}

将函数内部的标识符 a 改成 x

按照我们之前的方式,遍历 Identifier 节点,将标识符 a 改成 x,全局变量 a 的标识符也会改成 x

const fs = require('fs')
const traverse = require('@babel/traverse').default
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})


let ast = parser.parse(ast_code)
//
let visitor = {
    // 先找到所有的函数, 然后将函数内部的标识符 a 改为 x
    FunctionExpression: function(path){
        // path.traverse 可以将遍历的范围限制在局部
        // 使用 path 对象的 traverse 方法
        // 从当前遍历到的节点再往下选择节点去遍历并处理
        let func_visitor = {
            Identifier(path) {
                if (path.node.name === 'a') {
                    path.node.name = 'x'
                }
            }
        }
        path.traverse(func_visitor)
    }
}
traverse(ast, visitor)
//
let js_code = generator(ast, {
    retainLines: false,
    comments: true,
    compact: false,
}).code
fs.writeFileSync('New_demo.js', js_code, {
    encoding: 'utf-8'
})

将函数中的第一个参数改成标识符 z

let visitor = {
    FunctionExpression: function(path){
        // 函数中的参数都是放在 type 为 FunctionExpression 中的 params 节点中类型为 list
        // 第一个参数就为 params[0], 取到这个参数对应的标识符名字就为 params[0].name
        // console.log(path.node.params[0].name)
        let par_name = path.node.params[0].name
        // 再使用 path.traverse 进行局部遍历
        path.traverse(
            {
                Identifier(p){
                    // 如果传进来的参数和外部取到函数第一个参数标识符的名字一致
                    // 则将改标识符名字改为 x
                    console.log(this.par_name)  // this.par_name 为 path.traverse 第二个参数 
                    if(p.node.name === this.par_name) { p.node.name = 'z' };
                }
            },
            {par_name:par_name}
        )
    }
}

types

判断节点类型

判断节点类型有三种方式
  -  node节点的type属性
  -  types组件的方法(传入node节点类型)
  -  path对象的方法(判断path对象下的类型,一般情况下和node节点的类型是一致的)
types组件判断节点类型的方法
  - is类型
      - 返回布尔值
  - asert类型
      - 当前节点不符合要求,会抛出异常
  
最常用的是 is的版本,返回布尔值 
// file_name: demo.js
let a = 100;
let obj = {
    name: 'xiaojianbang',
    add: function(a, b){
        return a + b + 1000;
    },
    mul: function (c, b){
        return c * b + 1000;
    }
}
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})


let ast = parser.parse(ast_code)
//
traverse(ast, {
    // 遍历所有ast节点,遍历到节点的时候都会进入函数
    enter(path) {
        // 方式一, path.node.type 判断类型
        // if(path.node.type === 'Identifier' && path.node.name === 'a'){
        //     console.log(path.node.name)
        //     path.node.name = 'x'
        // }

        // 方式二, type.is.... 判断节点类型
        // (
        // 参数1: node节点,
        // 参数2: 可以用来传入一个对象,可以判断node节点指定属性的值
        // )
        // if (types.isIdentifier(path.node, {name: 'a'})) {
        //     console.log(path.node.name)
        //     path.node.name = 'x'
        // }

        // 方式三 path.is.... 判断节点类型
        // (参数1: 可以用来传入一个对象,可以判断node节点指定属性的值)
        if (path.isIdentifier({name: 'a'})) {
            console.log(path.node.name)
            path.node.name = 'x'
        }
    }
})
//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
    encoding: 'utf-8'
})

生成新的节点

// file_name: AST.js
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
//
/*
生成代码示例
let a = 100;
let obj = {
    a: 200,
    add: function(a, b){
        return a + b + 1000;
    },
    mul: function (c, b){
        return c * b + 1000;
    }
}
*/
// 编辑器建议使用 vscode,如果有报错删了重装 babel 库
// ctrl + 鼠标左键 方法名 可以进入对应的方法,查看生成该节点需要什么参数类型



// 生成标识符 a, obj, add, mul, b, c
// declare function identifier(name: string): Identifier;
// 生成 identifier 节点需要传入 字符串
ide_a = types.identifier('a')  // 标识符 a
ide_obj = types.identifier('obj')  // 标识符 obj
ide_add = types.identifier('add')  // 标识符 add
ide_mul = types.identifier('mul')  // 标识符 mul
ide_b = types.identifier('b')  // 标识符 b
ide_c = types.identifier('c')  // 标识符 c



// https://astexplorer.net/
// 将代码放到网站上查看语法树生成 let a = 100 需要什么节点类型;
// declare function variableDeclaration(kind: "var" | "let" | "const" | "using" | "await using", declarations: Array<VariableDeclarator>): VariableDeclaration;
// declare function variableDeclarator(id: LVal, init?: Expression | null): VariableDeclarator;
let demo_1 = types.variableDeclaration('let', [types.variableDeclarator(ide_a, types.numericLiteral(100))]) // let a = 100;
console.log(generator(demo_1).code)  // 生成的节点为 node 节点,所以需要 generator 组件将 ast 语法树转换成js代码
// let a = 100;


// declare function objectExpression(properties: Array<ObjectMethod | ObjectProperty | SpreadElement>): ObjectExpression;
// declare function objectProperty(key: Expression | Identifier | StringLiteral | NumericLiteral | BigIntLiteral | DecimalLiteral | PrivateName, value: Expression | PatternLike, computed?: boolean, shorthand?: boolean, decorators?: Array<Decorator> | null): ObjectProperty;
// declare function functionExpression(id: Identifier | null | undefined, params: Array<Identifier | Pattern | RestElement>, body: BlockStatement, generator?: boolean, async?: boolean): FunctionExpression;
// declare function blockStatement(body: Array<Statement>, directives?: Array<Directive>): BlockStatement;
// declare function returnStatement(argument?: Expression | null): ReturnStatement;
// declare function binaryExpression(operator: "+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=" | "|>", left: Expression | PrivateName, right: Expression): BinaryExpression;
let obj_1 = types.objectProperty(ide_a, types.numericLiteral(200))  // a: 200,
let obj_2 = types.objectProperty(ide_add, 
    types.functionExpression(
        null, 
        [ide_a, ide_b], 
        types.blockStatement([
            types.returnStatement(types.binaryExpression(
                "+",
                ide_a,
                types.binaryExpression("+", ide_b, types.numericLiteral(1000),           
                )        
            ))
        ])
))  // add: function (a, b) {return a + (b + 1000);}
let obj_3 = types.objectProperty(ide_mul, 
    types.functionExpression(
        null, 
        [ide_c, ide_b], 
        types.blockStatement([
            types.returnStatement(types.binaryExpression(
                "*",
                ide_c,
                types.binaryExpression("+", ide_b, types.numericLiteral(1000),           
                )        
            ))
        ])
))   // mul: function (c, b){return c * b + 1000;}

let demo_2 = types.variableDeclaration('let', [types.variableDeclarator(ide_obj, types.objectExpression([obj_1, obj_2, obj_3]))])

// 将生成的节点转换成 js代码并打印
console.log(generator(demo_2).code)  
/*
let obj = {
  a: 200,
  add: function (a, b) {
    return a + (b + 1000);
  },
  mul: function (c, b) {
    return c * (b + 1000);
  }
};
*/

valueToNode(方便的生成字面量)

支持生成的类型
  - undefined undefined
  - boolean 布尔值
  - null 空
  - string 字符串
  - number 数值
  - RegExp 正则表达式
  - ArrayExpression 数组表达式
  - object 对象
  - Expression 表达式语句

字面量可以用 valueToNode 可以很方便的生成

const types = require('@babel/types')
const generator = require('@babel/generator').default

console.log(types.valueToNode(100)); // 生成numericliteral节点  数值字面量100
console.log(types.valueToNode('xiaojianbang'))  // 生成stringliteral节点  字符串xiaojianbang
array_node = types.valueToNode([ // 数组表达式
    undefined,  // undefined
    true,  // 布尔值
    null,  // null
    'xiaojianbang', // 字符串 
    100,   // 数值
    /\w\s/g, // 正则表达式
    {a:100} // 对象
])
console.log(generator(array_node).code)

path 对象(重点)

path 与 Node 的区别

// file_name: demo.js
let Obj = {
    name: 'xiaojiangbang',
    add: function(a, b){
        return a + b + 1000;
    },
    mul: function (a, b) {
        return a * b + 1000;
    }
}
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})


let ast = parser.parse(ast_code)
//
traverse(ast, {
    Identifier(path){
        console.log(path.node)  // node 是path对象里的一个属性
        console.log(path.parentPath) // parentPath 是 path 对象里的父节点属性
        console.log(path.parent)  // parent 是 parentPath 对象里的一个属性
        // parentPath 与 parent 和 path 与 node 的关系是一样的,都是 path 对象里的属性 
        path.stop()  // path对象下的方法,当触发这个方法的时候就会停止遍历
    }
})

//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
    encoding: 'utf-8'
}) 

path中的方法

skip 与 stop 与 return
path.stop()
  -  将本次遍历执行完毕后,停止后续节点遍历(会将本次遍历代码完整执行)

path.skip()
  -  执行节点替换操作后,traverse 依旧能够遍历
  -  path.skip() 可以跳过替换后的节点遍历,避免不合理的递归调用,不影响后续节点遍历(skip位置后面代码会执行)

return
  -  跳过当前遍历的后续代码,不影响后续节点遍历(相当于continue)
  -  不建议使用,推荐使用 path
获取子节点 / path.get()
//
traverse(ast, {
    BinaryExpression(path){
        // 获取子节点
        console.log(path.node.left)
        console.log(path.node.right)
        console.log(path.node.operator)
      
        // 获取子path
        console.log(path.get('left'))
        console.log(path.get('right'))
        console.log(path.get('operator'))  // 实际上只是一个 + 号,但是也被包装成了path对象进行返回
        console.log(path.get('left.left.name'));  // 如果存在多级访问,需要以 . 连接
        
        // 这样子的操作是没有意义的
        // 假如你获取到的新的节点,后续还要调用 path 的方法去进行访问的话,那么就可以通过get去获取
        path.stop()
    }
}
)
//
判断 path 类型 / path.is…
path对象提供相应的方法来判断自身类型,使用方法与 types 组件差不多
  -  types组件判断的是 node
  -  path对象判断的是 path
//
traverse(ast, {
    BinaryExpression(path){
        // 获取到的新的节点,后续还要调用path的方法去进行访问的话,那么就可以通过get去获取
        console.log(path.get('left').isIdentifier())  // 是否为标识符类型
        console.log(path.get('right').isNumericLiteral({value:1000}))  // 是否为数值类型并且值是否为1000
        path.stop()
    }
}
)
//
节点转代码 / path.toString() / path + ‘’
很多时候需要在执行过程中把部分节点转为代码,而不是在最后才把整个AST转成代码

grenator组件也可以把 AST 中的一部分节点转成代码,这对节点遍历过程中的调试很有帮助
  -   grenator转换的是node节点
//
traverse(ast, {
    FunctionExpression(path){
        // generator 组件转换
        console.log(generator(path.node).code)  // grenator转换的是node节点
        
        // path 对象,更简便的转换方式
        console.log(path.toString());
        console.log(path + '')  // 隐式转换
        // path 对象是复写了toString() 方法的
        // path.node 只是一个普通的对象,并没有复写 toString 方法
        console.log(path.node + '')
        path.stop()
    }
})
//
替换节点属性
 替换节点属性与获取节点属性方法相同,只是改为赋值,但并非随意替换
   - 需要注意替换的类型要在运行的类型范围内,因此需要熟悉 ast 的结构
//
traverse(ast, {
    BinaryExpression(path){
        // 注意替换的类型要在允许的类型范围内,因此需要熟悉 ast 的结构
        // path.node.left = 'x' 这样替换是错误的
        path.node.left = types.identifier('x')
    }
})
//
替换整个节点 replaceWith() / replaceWithMutiple() / replaceInline() / replaceWithSourceString()
replaceWith()

节点替换节点,并且是一换一

//
traverse(ast, {
    BinaryExpression(path){
        path.replaceWith(types.valueToNode('xiaojianbang'))
        path.stop()
    }
})
//

运行结果

let a = 100;
let obj = {
  name: 'xiaojianbang',
  add: function (a, b) {
    return "xiaojianbang";
  },
  mul: function (c, b) {
    return c * b + 1000;
  }
};
replaceWithMutiple()

节点替换节点,并且是一换多

//
traverse(ast, {
    BinaryExpression(path){
        // 节点替换节点,并且是一换多
        path.replaceWithMultiple([
            types.valueToNode('xiaojianbang1'),
            types.valueToNode('xiaojianbang2'),
            types.valueToNode('xiaojianbang3'),
        ])
        path.stop()
    }
})
/

运行结果

let a = 100;
let obj = {
  name: 'xiaojianbang',
  add: function (a, b) {
    return "xiaojianbang1", "xiaojianbang2", "xiaojianbang3";
  },
  mul: function (c, b) {
    return c * b + 1000;
  }
};
replaceInline()
  replaceWith() 与 replaceWithMutiple() 的结合
    -  当传给的参数是一个参数的时候,就相当于是 replaceWith()
    -  当传给的参数是数组的时候,就相当于是 replaceWithMutiple()

用法一

//
traverse(ast, {
    BinaryExpression(path){
        // 当传给的参数是一个参数的时候
        // 就相当于是 replaceWith
        path.replaceInline(types.valueToNode('xiaojianbang1'))
        path.stop()
    }
})
//

运行结果

let a = 100;
let obj = {
  name: 'xiaojianbang',
  add: function (a, b) {
    return "xiaojianbang1";
  },
  mul: function (c, b) {
    return c * b + 1000;
  }
};

用法二

//
traverse(ast, {
    BinaryExpression(path){
        // 当传给的参数是一个数组的时候(一个或多个参数)
        // 相当于是 replaceWithMutiple
        path.replaceInline([
            types.valueToNode('xiaojianbang1'),
            types.valueToNode('xiaojianbang2'),
            types.valueToNode('xiaojianbang3'),
        ])
        path.stop()
    }
})
//

运行结果

let a = 100;
let obj = {
  name: 'xiaojianbang',
  add: function (a, b) {
    return "xiaojianbang1";
  },
  mul: function (c, b) {
    return c * b + 1000;
  }
};
replaceWithSourceString()

用字符串当成代码去替换节点

//
traverse(ast, {
    ReturnStatement(path){
        // 代码生成示例
        /*
        let a = 100;
        let obj = {
            name: 'xiaojianbang',
            add: function(a, b){
                return function(){
                    return a + b + 1000
                }()
            },
            mul: function (c, b){
                return function(){
                    c * b + 1000
                }()
            }
        }
        */

        // 获取path对象可以很方便的把节点转为代码(隐式转换)
        // path.get 方法返回的就是 path 对象
        console.log(path.get('argument') + '')  // path 取 return 语句的返回值

        // 使用 node 节点转代码需要用到 generator 组件
        // 例:  ret_arg = generator(path.node.argument).code
        console.log(generator(path.node.argument).code)  // node 对象取 return 语句的返回值
        console.log('==========')
        let ret_arg = path.get('argument') + ''
        path.replaceWithSourceString('function(){return ' + ret_arg + '}()')
        path.skip()  // 跳过替换后的节点遍历 
        // 因为遍历的是 ReturnStatement 节点,替换后的节点也是 ReturnStatement 节点
        // path.skip 会跳过替换后的节点遍历,但是不影响后续的遍历
    }
})
//
删除节点 remove

删除当前节点

//
traverse(ast, {
    ReturnStatement(path){
        path.remove()
    }
})
//

结果

let a = 100;
let obj = {
  name: 'xiaojianbang',
  add: function (a, b) {},
  mul: function (c, b) {}
};
插入节点 insertBefore / insertAfter
//
traverse(ast, {
    ReturnStatement(path){
        path.insertBefore(types.valueToNode('insertBefore')) // 节点前插入代码
        path.insertAfter(types.valueToNode('insertAfter'))  // 节点后插入代码

        // 可以插入多个成员,传入数组
        path.insertBefore([types.valueToNode('insertBefore1'), types.valueToNode('insertBefore2'),])
        path.insertAfter([types.valueToNode('insertAfter1'), types.valueToNode('insertAfter2'),])
    }
})
//
let a = 100;
let obj = {
  name: 'xiaojianbang',
  add: function (a, b) {
    "insertBefore"
    "insertBefore1"
    "insertBefore2"
    return a + b + 1000;
    "insertAfter1"
    "insertAfter2"
    "insertAfter"
  },
  mul: function (c, b) {
    "insertBefore"
    "insertBefore1"
    "insertBefore2"
    return c * b + 1000;
    "insertAfter1"
    "insertAfter2"
    "insertAfter"
  }
};
父级path: parent / parentPath

path.parentPath.node 等价于 path.parent

//
traverse(ast, {
    ReturnStatement(path){
        console.log(path.parentPath.node);
        console.log(path.parent)
        console.log('==============================')
    }
})
//

同级 path

容器 container
container(容器)
listKey(容器名)
key (当前节点在容器中的索引)
// file_name: demo.js
let a = 100;
let obj = {
  name: 'xiaojianbang',
  add: function(x, y){
    let a = 100
    let b = 200;
    return x + y + a + b + 1000;
  },
  mul: function (c, b){
    return c * b + 1000;
  }
}
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})


let ast = parser.parse(ast_code)
//
traverse(ast, {
    ReturnStatement(path){
        console.log(path + '');
        console.log(path.container);
        console.log(path.listKey);
        console.log(path.key);
        console.log('==========');
    }
})
//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
    encoding: 'utf-8'
}) 

container 并非一直都是数组

// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
    encoding: 'utf-8'
})


let ast = parser.parse(ast_code)
//
traverse(ast, {
    ObjectExpression(path){
        console.log(path + '');
        console.log('container:  ', path.container);
        console.log('listKey:', path.listKey);
        console.log('key:', path.key);
        console.log('==========')
    }
})
//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
    encoding: 'utf-8'
}) 

对于这种情况,可以把它(container)当成不是容器(它是没有兄弟节点的)

分析的时候只需要关注 container 是数组的情况(代表它有兄弟(同级)节点)

path.inList

用于判断是否有同级节点
当 container 为数组,但是只有一个成员时,也会返回 true

path.container / path.listKey / path.key

path.key 获取当前节点在容器中的索引
path.container 获取容器(包含所有同级节点的数组)
path.listKey 获取容器名

path.getSibling(index)

用于获取同级path,其中参数 index 即是容器数组中的索引

path.getAllNextSiblings

获取所有后续的节点

path.getNextSibling

获取下一个节点

path.getAllPrevSiblings

获取所有前面的节点

path.getPrevSibling

获取上一个节点

unshiftContainer 与 pushContainer

unshiftContainer 在容器的头部插入节点
pushContainer 在容器的尾部插入节点

//
traverse(ast, {
  ReturnStatement(path){
    console.log(path.listKey)
    console.log(path + '')
    // 在容器的头部插入节点
    path.parentPath.unshiftContainer('body', types.expressionStatement(types.stringLiteral('unshiftContainer')))  // 插入一个
    path.parentPath.unshiftContainer('body', [  // 插入多个
      types.expressionStatement(types.stringLiteral('unshiftContainer1')),
      types.expressionStatement(types.stringLiteral('unshiftContainer2')),
      types.expressionStatement(types.stringLiteral('unshiftContainer3')),
    ])

    // 在容器的尾部插入节点
    path.parentPath.pushContainer('body', types.expressionStatement(types.stringLiteral('pushContainer')))  // 插入一个
    path.parentPath.pushContainer('body', [  // 插入多个
      types.expressionStatement(types.stringLiteral('pushContainer1')),
      types.expressionStatement(types.stringLiteral('pushContainer2')),
      types.expressionStatement(types.stringLiteral('pushContainer3')),
    ])

    path.stop()
  }
})
//
//
traverse(ast, {
    ReturnStatement(path){
        console.log('=================path下的属性===========================');
        console.log('inList:  ', path.inList)  // container 是否为数组
        console.log('listKey', path.listKey)  // 输出容器名
        console.log('container: ', path.container);  // 输出 container(容器)
        console.log('key:  ', path.key);  // 输出 当前节点在容器中的索引


        console.log('==================path.getSibling======================');
        // path.getSibling 用于获取同级path
        console.log('getSibling: ', path.getSibling(path.key))  // 输出当前节点
        console.log('path.getSibling(path.key).toString():  ', path.getSibling(path.key) + '')

        console.log('getSibling: ', path.getSibling(path.key + 1)) // 输出当前节点的下一个节点
        console.log('path.getSibling(path.key + 1).toString():  ', path.getSibling(path.key + 1) + '') // 输出当前节点的下一个节点

        console.log('path.getSibling(path.key - 1):  ', path.getSibling(path.key - 1)) // 输出当前节点的上一个节点
        console.log('path.getSibling(path.key - 1).toString():  ', path.getSibling(path.key - 1) + '') // 输出当前节点的上一个节点



        console.log('==================path.getPrevSibling====================');
        console.log('getPrevSibling:  ', path.getPrevSibling() + '')  // 获取上一个节点 path.getSibling(path.key - 1)
        console.log('getAllPrevSiblings:  ', path.getAllPrevSiblings() + '')  // 获取前面所有节点

        console.log('=================path.getNextSibling======================');
        console.log('getNextSibling: ', path.getNextSibling() + '')  // 获取下一个节点 path.getSibling(path.key + 1)
        console.log('getAllNextSiblings: ', path.getAllNextSiblings() + '')  // 获取后续所有节点

        path.stop()
    }
})
//

scope 详解

scope 提供了一些属性和方法
  - 可以方便的查找标识符的作用域
  - 获取标识符的所有引用
  - 修改标识符的所有引用
  - 标识符是否为参数,是否为常量,如果不是常量,也可以知道在哪里修改了它

demo.js

const a = 1000;
let b = 2000;
let obj = {
    name: 'xiaojianbang',
    add: function(){
        a = 400;
        b = 300;
        let e = 700;
        function demo(){
            let d = 600;
        }
        demo();
        return a + a + b + 1000 + obj.name
    }
}
获取标识符作用域范围 scope.block
//
traverse(ast, {
    Identifier(path){
        if(path.node.name == 'e'){
            console.log('当前遍历到的节点的完整代码:  ', path.parentPath.parentPath + '')
            // path.scope.block 记录的是当前标识符的作用域范围
            console.log(path.scope.block); // 返回的是 node 节点
            console.log('e标识符的作用域:\n', generator(path.scope.block).code);
        }
    }
})
//

在这里插入图片描述

可以看到 e 标识符的作用域范围在 function(){} 函数内部

获取函数作用域范围 scope.block 方式获取

对于函数来讲,如果通过 path.scope.block 的方式去获取作用域范围的话,需要从父节点入手
还可以使用 scope 对象下的 getBinding 方法获取
(获取作用域范围基本上都是使用 getBinding 的方式获取)

//
traverse(ast, {
    FunctionDeclaration(path){
        // 遍历函数声明语句
        console.log('当前遍历到的函数名:  ', path.node.id.name)  // 当前遍历的到是 demo 函数,demo.js 代码中只有 demo 函数为 FunctionDeclaration 节点
        // 打印当前节点的作用域范围(对于函数来讲,如果通过 path.scope.block 的方式去获取作用域范围的话,需要从父节点入手)
        console.log('path.scope.block:  \n', generator(path.scope.block).code); 
        console.log('==========================================================');
        console.log('path.parentPath.scope.block:  \n', generator(path.parentPath.scope.block).code);
    }
})
//
scope.getBinding()

scope.getBinding() 接收一个类型为 string 的参数,用来获取对应标识符的绑定
如果没有获取到对应标识符的绑定,返回 undefined

//
traverse(ast, {
    FunctionDeclaration(path){
        // 获取标识符a的绑定
        let bindinga = path.scope.getBinding('a')
        console.log(generator(bindinga.scope.block).code);  // 打印 a 标识符的作用域范围

        // 获取 demo 标识符的绑定
        let bindingdemo = path.scope.getBinding('demo')
        console.log(generator(bindingdemo.scope.block).code);  // 打印 demo 标识符的作用域范围

    }
})
//
scope.getOwnBinding()

获取当前节点自己的绑定,也就是不包含父级作用域中定义的标识符绑定
但是该函数会得到子函数中定义的标识符绑定

//
function TestOwnBanding(path){
    path.traverse({
        Identifier(p){
            let name = p.node.name
            // 判断该标识符的作用域是只是在当前函数
            console.log(name, !!p.scope.getOwnBinding(name))
        }
    })
}
traverse(ast, {
    FunctionExpression(path){
        // 当前遍历到的是 demo 函数
        // 当前遍历到的 FunctionExpression 节点
        console.log('当前遍历到的 FunctionExpression 节点\n',
            path + ''
        )
        console.log('===============================')
        TestOwnBanding(path)
    },
})
//
//
// 该函数会得到子函数中定义的标识符绑定
// 处理得到子函数中定义的标识符绑定
// 过滤掉子函数中定义的标识符的绑定
function TestOwnBinding(path){
    path.traverse({
        Identifier(p){
            let name = p.node.name;  // 获取标识符的名字
            let binding = p.scope.getBinding(name);  // 获取标识符对应的绑定
            // 如果绑定存在的话 &&
            // 输出标识符的名字, 输出作用域范围是否与path相等
            // binding.scope.block 当前标识符的作用域范围
            // path + '' 遍历到的节点转 js 代码
            binding && console.log(name, generator(binding.scope.block).code == path + '')
            /*
            当前遍历到的 FunctionExpression 函数表达式
            add: function(a){
                a = 400;
                b = 300;
                let e = 700;
                function demo(){
                    let d = 600;
                }
                demo();
                return a + a + b + 1000 + obj.name
            }
            */
           /*
            例如标识符a 标识符a在全局也有一个 const a = 1000;
            但是函数内部定义了一个参数 a, 所以a就是局部变量
            获取 a 对应的作用域范围,并将获取到的 scope.block 转成js代码
            就和当前遍历到的节点相等,代表 a 是当前节点的绑定
           */ 
        }
    })
}
traverse(ast, {
    FunctionExpression(path){
        console.log(path)
        TestOwnBinding(path);
    }
})
//
referencePaths
referncePaths(记录了标识符被引用的地方)
  - 有了 referncePaths 这个对象以后,修改标识符对应的引用就会显得很容易了
    - 在还原的时候是会经常用到的

假如标识符被引用,referncePaths 中会存放所有引用该标识符节点的 path 对象

//
traverse(ast, {
    FunctionExpression(path){
        let binding = path.scope.getBinding('a')  // 获取标识符a的绑定
        console.log(binding)
    }
})
//

在这里插入图片描述

//
traverse(ast, {
    FunctionExpression(path){
        let binding = path.scope.getBinding('a')  // 获取标识符a的绑定
        console.log(binding.referencePaths)
        binding.referencePaths[0].replaceWithSourceString('y + h')  // 替换第一个(被引用到的地方)节点
    }
})
//
constantViolations

假如标识符有被修改,constantViolations 中会存放所有修改该标识符节点的 path 对象

//
traverse(ast, {
    FunctionExpression(path){
        let binding = path.scope.getBinding('a')  // 获取标识符a的绑定
        console.log(binding.constantViolations[0].node)
    }
})
//

在这里插入图片描述

//
traverse(ast, {
    FunctionExpression(path){
        let binding = path.scope.getBinding('a')  // 获取标识符a的绑定
        console.log(binding.constantViolations[0] + '');  // 获取到标识符 a 被修改的地方
        console.log(binding.constantViolations[0].node);  // 查看对应的 node 节点

        // 修改节点,将 400 改成 500
        // 1
        binding.constantViolations[0].node.right.value = 500
        // 2
        binding.constantViolations[0].replaceWithSourceString('a=500')
    }
})
//
scope.rename(标识符重命名)
scope.rename 可以将标识符重命名
这个方法会同时修改所有引用该标识符的地方

将 add 函数中的 b 变量重命名为 x

//
traverse(ast, {
    FunctionExpression(path){
        let b = path.scope.getBinding('b')  // 获取表示符b的引用
        console.log(b.path.node.id.name)
        console.log(b.path + '')
        b.scope.rename('b', 'x')
    }
})
//
scope.generateUidIdentifier()
//
traverse(ast, {
    FunctionExpression(path){
        // 可以保证标识符永远都不重名
        let uid1 = path.scope.generateUidIdentifier('uid')  // _uid
        console.log(uid1)
        let uid2 = path.scope.generateUidIdentifier('uid')  // _uid2
        console.log(uid2)

        let binding = path.scope.getBinding('b')
        // generateUidIdentifier 返回的是 Identifier 节点
        // 替换的时候需要的是字符串。所以取里面生成的字符串就可以替换了
        binding.scope.rename('b', path.scope.generateUidIdentifier("uid").name)
    }
})
// //

在这里插入图片描述

scope 的其他方法
  1. hasBinding(‘a’)
    a. 查询是否有标识符 a 的绑定,返回 true 或 false
    b. 可以用 scope.getBinding(‘a’) 代替
    ⅰ. scope.getBinding(‘a’) 返回 undefined 的时候,等同于 scope.hasBinding(‘a’) 返回 fasle
  2. hasOwnBinding(‘a’)
    a. 查询是否有自己的绑定,返回 true 或 false
    b. 可以用 scope.getOwnBinding(‘a’)代替他
    ⅰ. scope.getOwnBinding(‘a’) 返回 undefined 等同于 hasOwnBinding 返回 false
  3. getAllBindings()
    a. 获取当前节点的所有绑定,会返回一个对象,
    b. 该对象以标识符为属性名,对应的 Binding 为属性值
  4. hasReference(‘a’)
    a. 查询当前节点中是否有 a 标识符的引用,返回 true 或 false
  5. getBindingIdentifier(‘a’)
    a. 获取当前节点中的绑定的 a 标识符,返回的是 identifier 的 Node 对象
    b. 同样地,这个方法也有 Own 版本 getOwnBindingIdentifier(‘a’)

遍历作用域

  1. scope.traverse 可以用来遍历作用域
  2. 可以使用 Path 对象中的 scope
    a. 获取函数作用域的时候,需要用到 parentPath
  3. 也可以使用 Binding 中的 scope(推荐使用)
    a. 不管是函数还是变量,都能获取到正确的作用域
    b. Binding(str).scope 的方式更加的灵活
  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值