必须在作用域链完整、绑定关系明确前提下对局部声明标识符做有约束重命名,否则混淆后代码易报错;不能仅解析ast后批量改名,因identifier节点缺乏语义角色信息,盲目替换会破坏全局引用、导入绑定、属性访问及作用域层级等关键逻辑。

直接操作 AST 实现业务逻辑混淆,不是“解析完就替换”,而是必须在作用域链完整、绑定关系明确、外部引用可识别的前提下,对局部声明标识符做有约束的重命名——否则混淆后代码大概率运行报错或行为异常。
为什么不能用 acorn.parse 解析完就遍历 Identifier 节点批量改名
因为 Identifier 节点本身不携带语义角色信息:它可能是变量声明、函数名、参数、属性访问名、全局引用,甚至只是字符串字面量里的拼接片段。盲目替换会破坏以下关键逻辑:
-
console.log中的console是全局对象引用,不是变量,不能混淆 -
import { API_KEY } from './config'中的API_KEY是导入绑定,需保留原名或映射到导出名,不能简单缩写 -
obj['prop']和obj.prop中的prop语义不同:后者若未被声明为局部变量,属于字面量属性名,混淆即引入运行时错误 - 箭头函数参数
(x) => x + 1的x是绑定标识符,但若外层已有同名var x,则需区分作用域层级,否则替换冲突
如何用 @babel/parser + @babel/traverse 安全识别可混淆节点
核心是借助 Babel 的 scope 对象,而不是手动维护作用域栈。必须显式启用 sourceType: 'module',否则 import/export 无法正确建模作用域。
- 只处理
binding.kind === 'local'或'param'的标识符,跳过'module'(导入绑定)、'hoisted'(函数声明提升)和'unknown' - 对
VariableDeclarator.id、FunctionDeclaration.id、ArrowFunctionExpression.params等声明位置的节点,调用path.scope.getBinding(node.name)获取绑定对象再判断 - 遇到
/* @no-obfuscate */注释紧邻声明前时,通过path.node.leadingComments检查并跳过整条声明 - 避免修改
MemberExpression.property中computed === false的节点,除非已确认该属性名在当前作用域内被显式声明为变量(如const prop = 'data'; obj[prop])
混淆名生成必须满足三个硬约束
短名不是越短越好,更不是随机打乱。生产环境混淆要求名称在作用域内唯一、跨作用域可区分、且不覆盖任何保留字或内置名。
- 每个作用域实例维护独立的
nameMap: Map<string string></string>,键为原始名,值为混淆后名(如'userData' → '_u') - 生成策略推荐用计数器+小写字母组合(
_a,_b, ...,_aa),而非纯随机字符串——便于调试时反向映射 - 必须用
path.replaceWith()替换节点,而非直接赋值node.name;否则 Babel 不会重新绑定引用,后续binding.referencePaths将丢失或错位 - 每次替换前检查目标名是否为 JS 保留字(用
is-reserved-word库)或内置对象名(如'JSON','Promise','fetch'),跳过这些情况
最容易被忽略的是混淆后的源码映射(source map)与运行时调试支持。如果只改 AST 不生成对应 source map,线上报错堆栈将指向混淆后代码,失去定位能力。而加了 source map 又可能暴露原始结构——这个权衡点不在 AST 遍历本身,而在 @babel/generator 调用时的 sourceMaps 和 inputSourceMap 参数配置,必须和构建流程深度耦合。










