直接替换变量名会出错,因正则无法区分作用域和语义,而AST能精准识别Identifier节点并仅混淆局部变量和参数,跳过属性名、内置方法、全局变量等。
手动遍历字符串做正则替换,var a = 1; console.log(a); 中的 a 会被替换成新名字,但 console.log 里的 log 也可能被误伤——因为正则不区分作用域和语义。AST 能精准识别 Identifier 节点,并判断它是否为可混淆的局部变量或函数参数,跳过属性名、内置方法、全局变量等。
@babel/parser 解析并遍历 AST 的关键步骤不用写词法分析器,Babel 提供了稳定、兼容性好的解析能力。重点不是“怎么生成 AST”,而是“怎么安全标记和替换”:
VariableDeclarator(如 let x = 1 中的 x)和 FunctionDeclaration/ArrowFunctionExpression 的参数节点MemberExpression 中的 property(如 obj.name 的 name),否则会破坏对象访问scope.bindings 判断标识符是否为局部绑定(Babel 的 scope 对象在遍历时自动可用)this、arguments、super 等上下文关键字,它们不是普通 Identifier
示例节选(Babel 插件写法):
export default function({ types: t }) { return { visitor: { Identifier(path) { const { node, scope } = path; if (node.name === 'this' || node.name === 'arguments') return; if (!scope.hasBinding(node.name)) return; // 不是当前作用域声明的,不碰 if (scope.getBinding(node.name).kind !== 'let' && scope.getBinding(node.name).kind !== 'const' && scope.getBinding(node.name).kind !== 'var') return; // 此处生成新名字,如 'a' → '_0x1a2b' path.replaceWith(t.identifier('_0x1a2b')); } } };}
scope.hasBinding() 和 scope.getBinding() 的实际差异前者只是快速判断名字是否存在绑定,后者返回完整绑定对象,含 kind(var/let/const/param)、referencePaths(所有引用位置)、path(声明节点)。混淆时必须用 getBinding(),否则无法区分 let x 和 obj.x ——两者都通过 hasBinding('x') 返回 false,但后者根本不在作用域中。
立即学习“Java免费学习笔记(深入)”;
scope.hasBinding('x') 在 function f(x) { let x = 2; } 中对第一个 x(参数)返回 true,对第二个也返回 true,但它们属于不同绑定scope.getBinding('x') 获取其声明路径,再决定是否重命名kind 是 'param',不是 'let',漏判会导致函数参数没被混淆不是 AST 没走通,而是语义被破坏:
class A { method() {} } 中的 method 当作可混淆标识符——其实它是 MethodDefinition 的 key,不是 Identifier 节点;需额外处理 ObjectMethod 和 ClassMethod 的 key 属性,且仅当 key.type === 'Identifier' 且非计算属性时才考虑import { foo } from './x' 中的 foo,导致模块导入失效——ESM 的导入绑定是只读的,不能重命名;应跳过所有 ImportSpecifier 和 ExportSpecifier 中的 local 和 exported
{x: '_a', y: '_b'})混淆,但没做作用域隔离,导致嵌套函数内同名变量被统一替换,破坏闭包逻辑;必须为每个 Scope 实例维护独立的映射缓存最易被忽略的是 this 绑定上下文:箭头函数里写的 this 不是标识符,但若混淆了外层函数名,可能让调试时的调用栈完全不可读——混淆工具不该动函数名,除非明确支持 functionName 选项并默认关闭。