前言
UglifyJS会对JS文件的变量名进行混淆处理,要理解Javascript变量混淆的细节,我们需要回答以下几个问题:
1.遇到一个变量myName,我们怎么知道这个myName变量要不要混淆
2.混淆名字怎么生成才合适,新的名字替换旧的名字时有什么要注意的地方?
3.哪些关键字会产生一个作用域?
4.作用域链跟符号表在UglifyJS里边是怎么体现?
5.UglifyJS混淆的过程是什么样?
我们先梳理一下这5个问题,最后贴出我阅读UglifyJS在这部分的实现时做的代码注释。
1.遇到一个变量myName,我们怎么知道这个myName变量要不要混淆
Javascript里边涉及到名字分为三种:变量名、函数名、标签名,下文统称为名字。
为了混淆某个名字,我们必须知道这个名字在当前作用域以及作用域链上的声明情况以及使用情况。我们先从变量的名字混淆开始讨论。
举个简单的例子,JS文件内容是:var myName = {}; myName.prop = val;
这里myName这个名字可以被混淆成别的名字,但是val这个变量就不能被混淆,因为它是全局变量,有可能在别的文件里边声明定义了。
同时我们知道如果在当前文件定义了一个全局变量,有可能会被另一个文件所引用,因此这个全局变量的名字也不能被混淆。
当然这里适用于函数名跟标签名。
规则1.1:只有在作用域链上边的声明过的名字才可以混淆;当前文件声明的全局变量的名字不能混淆
对于一个函数声明:function func(argA, argB, argC){}
Javascript这里进入func之后其实就进入了func的作用域,我们知道argA/argB/argC其实就是在这个func作用域上声明的变量,
规则1.2:函数声明时的参数名可以混淆。
还可以发现一个特殊的地方,就是:try{ } catch(e) { }
规则1.3:catch后边参数列表的名字可以混淆。
举个例子:
function A(){
var myName = "A";
function B(){
myName = "B";
with(obj){
myName = "with";
}
}
}
由于with会改变当前的作用域链,我们知道在with里边,如果obj具有myName这个属性的话,那myName = "with"其实就等价于obj.myName = "with";
如果是这种情况混淆了myName这个名字,运行时可能就不再对obj的myName属性进行赋值了。同理如果myName混淆成名字e的话,刚刚好obj有个属性名字叫做e,也可能会引起运行时错误。
规则1.4:在使用了with的作用域链上的所有变量名都不能混淆。
function A(){
var myName = "A";
function B(){
myName = "B";
eval("myName = 1;");
}
}
因为eval是在运行时才知道执行的字符串的内容,因此在静态分析的时候并不能知道eval后边的字符串引用了什么变量,如果在当前作用域链上混淆了某些变量,可能引起eval的时候会有运行时找不到变量的错误。当然再复杂的情况就是eval里边又使用eval跟with嵌套
规则1.5:在使用了eval的作用域链上的所有变量名都不能混淆。
2.混淆名字怎么生成才合适,新的名字替换旧的名字时有什么要注意的地方?
如果我明确了一个变量myName需要被混淆,那最后它应该变成什么样的名字呢?首先肯定是越短越好,这样可以更有效的减少JS文件体积,下载JS速度也相应会提高。
因此简单的方案就是我从 [a-z][A-Z]$_ 这54个字母中取一个作为作为变量名即可,如果当前作用域声明的变量超过了54个,那就需要从[a-z][A-Z]$_[0-9]这64个字母中再去取第二个字母,如果还不够就接着取第三个字母。看完UglifyJS源码,觉得最牛逼的一点是,竟然为了考虑到gzip后的JS文件更小,其使用的混淆名字的顺序是:"etnrisouaflchpdvmgybwESxTNCkLAOM_DPHBjFIqRUzWXV$JKQGYZ0516372984"
UglifyJS的实现是,当前作用域得到的第1个混淆名为e,第2个混淆名是t……第54个是Z,第55个是et,第56个是tt……
讨论完怎么生成名字的规则后需要讨论混淆的规则了,混淆必须根据当前作用域的一些信息才能得以进行,首先这个规则最简单:
规则2.1:当前作用域不同变量混淆后的变量名不能重复,同时混淆后的名字不能是关键字。
其次要考虑以下几种在作用域链上的特殊情况:
场景1. 作用域B里边引用了作用域A作用域声明的变量name,因此作用域B里边就不能再使用name混淆后的变量名字e,否则会出现下图右侧那样的问题:
这个情况只要作用域B是在作用域A的嵌套底下才会出现,因此:
规则2.2:作用域B的祖宗作用域是作用域A,作用域B如果引用作用域A的变量name,而name变量被混淆后的名字为e,则作用域B里边不得再使用e来作为其下所有变量的混淆名字。
场景2.作用域A可能用到了一个全局变量e,因此我们不能将它混淆为其他名字,接着作用域B里边有e的引用,这个时候作用域B里边就不能再使用name混淆后的变量名字e,否则会出现下图右侧那样,e.b实际是引用了作用域B的e变量
这个情况只要作用域B是在作用域A的嵌套底下才会出现,因此:
规则2.3:作用域B的祖宗作用域是作用域A,作用域B如果引用作用域A不参与混淆的变量e,则作用域B里边不得再使用e来作为其下所有变量的混淆名字。
场景3.作用域A可能一个全局变量name,但是我们在作用域A的祖宗作用域中都找不到name的声明,这时候就不能把name给混淆掉,否则会出现右图那样,实际上没有e这个全局变量
规则2.4:不能混淆全局变量。
3.哪些关键字会产生一个作用域?
在UglifyJS里边采用with_new_scope这个函数来为AST树枝生成作用域信息。
可以从其源码中看到两个地方会产生作用域信息。
规则3.1:整个JS文件处于一个全局作用域,UglifyJS称为toplevel。
规则3.2:function关键字声明的函数内部处于一个作用域。
规则3.3:作用域A可以嵌套作用域B,这里可称之为作用域链,为了叙述方便,我把作用域B为作用域A的后代,变量的声明是沿着作用域链从低向上找到对应的定义。
也许会比较奇怪两个问题:
1.with块里边不算作用域?
其实从Javascript的角度上来说,with是会改变当前作用域的。
在with(obj){ /**/ }的块里边,其实是位于一个新的作用域上,作用域的符号表就是obj这个对象。
但是为什么UglifyJS不把with认为是一个作用域?原因是with是在运行时改变了当前作用域链,UglifyJS在静态分析源代码的时候根本没法得知运行时的信息,因此没法把它当做作用域来看待,因为静态分析没法知道在with块里边怎么混淆变量名字,因此才有了规则1.4。
2.catch块不算作用域?
先看一个简单的Demo
其实这个例子不能说明问题,你应该始终把catch块认为是一个with块!
在进入catch块的时候,Javascript确实会生成一个作用域,这个作用域跟with的参数一样,是一个对象,也许你会奇怪这个对象有什么属性?答案就是:catch后边带的参数名是什么,这个对象就有那个属性。
上边的Demo中,进入catch的时候,实际上就是把一个对象Obj = {e:{}}放到了当前作用域链上。
然后在解析myName的时候,发现Obj没有,就回到了父亲作用域上找myName的声明,如果找不到,就在父亲作用域上边声明myName这个变量。
解析e变量的时候,发现e在当前的Obj属性有,所以在catch里边能够找到e的声明。
但是同with关键字一样,UglifyJS没法静态分析得到这个信息,因此它不会为catch这个AST树枝生成作用域信息。但是!catch跟with不同的地方就是,catch在静态分析的时候是可以知道在catch块作用域里边声明的变量了,没错,就是catch后边带的参数名字e,因此UglifyJS是会对这个变量进行混淆处理的。
4.作用域链跟符号表在UglifyJS里边是怎么体现?
UglifyJS的实现里边是使用Scope作为作用域类,这边罗列一下其属性,详细的实现见文章末尾的代码注释:
- names = {} 表示在当前作用域下声明的变量函数的名字,也即是我们常常说的符号表!
- mangled = {} 混淆前变量以及混淆后变量的映射表,变量myName混淆后得到变量e,那么mangled["myName"] == "e"。
- rev_mangled = {} 跟mangled反过来,为了可以反查出混淆前变量的名字是什么。
- cname =
- refs = {} 记录着当前作用域内引用的信息,ref["a"] = <Scope B>
-
表示当前作用域使用了作用域链上B作用域所声明的a变量。
- uses_with = true/false 表示当前作用域有没有使用了with块,如果有的话,这条作用域链上的变量都不能混淆。
- uses_eval = true/false 表示当前作用域有没有使用eval函数,如果有的话,这条作用域链上的变量都不能混淆。
- parent = <Scope> 父亲作用域是谁。
- children = [<Scope>] 孩子作用域列表。
- level = <int> 作用域嵌套深度,全局作用域为0。
5.UglifyJS混淆的过程是什么样?
在语法分析之后得到AST树,UglifyJS就会开始遍历AST树,然后为某些节点生成作用域信息。
接着重新遍历AST树,再混淆里边的变量名,如下图:
UglifyJS的实现中是采用一个叫做ast_walker来遍历AST树,调用者可以传递不同的树枝遍历器给它,以实现不同的遍历效果,这个实现非常巧妙,可以从上图中看到,四个对AST树的操作其实底层都是需要遍历AST树的,而且每次对树的处理不一样,例如ast_add_scope要为树枝绑定作用域信息,ast_mangle要把叶子节点名字混淆掉,ast_squeeze要优化树枝大小,最后的gen_code要把树枝输出成字符串。
ast_walker对象是通过with_walkers这个API来重写遍历器:
遍历AST树的时候,可以重定义遍历器,对树枝进行处理:
代码注释
AST树遍历
//遍历AST树
//AST = ["toplevel", ["name", [xxx]]]
//一般数组的第一个是当前语法规则
//后边几个为语法规则对应的值
//例如函数的AST树枝就是这样表示:["function", "func_name", arguments, body],其中arguments为数组,body是另一个AST子树
function ast_walker() {
function _vardefs(defs) {
return [ this[0], MAP(defs, function(def){
var a = [ def[0] ];
if (def.length > 1)
a[1] = walk(def[1]);
return a;
}) ];
};
function _block(statements) {//语句块
var out = [ this[0] ];
if (statements != null)
out.push(MAP(statements, walk));//遍历所有语句
return out;
};
//默认的树枝遍历器
//从这里就可以看出了整个AST树枝的组成结构了
//叶子节点(例如数值就是所谓的叶子节点)遍历器都是默认不处理 返回原有的树枝结构
//如果节点不是叶子,,那么这个树枝的子树还需要递归遍历,例如:toplevel, function
//下边再针对某些特殊的地方做注释
var walkers = {
"string": function(str) {
return [ this[0], str ];
},
"num": function(num) {
return [ this[0], num ];
},
"name": function(name) {
return [ this[0], name ];
},
"toplevel": function(statements) {
return [ this[0], MAP(statements, walk) ];
},
"block": _block,
"splice": _block,//貌似没有这个状态?
"var": _vardefs,
"const": _vardefs,
"try": function(t, c, f) {
return [
this[0],
MAP(t, walk),
c != null ? [ c[0], MAP(c[1], walk) ] : null,
f != null ? MAP(f, walk) : null
];
},
"throw": function(expr) {
return [ this[0], walk(expr) ];
},
"new": function(ctor, args) {
return [ this[0], walk(ctor), MAP(args, walk) ];
},
"switch": function(expr, body) {
return [ this[0], walk(expr), MAP(body, function(branch){
return [ branch[0] ? walk(branch[0]) : null,
MAP(branch[1], walk) ];
}) ];
},
"break": function(label) {
return [ this[0], label ];
},
"continue": function(label) {
return [ this[0], label ];
},
"conditional": function(cond, t, e) {
return [ this[0], walk(cond), walk(t), walk(e) ];
},
"assign": function(op, lvalue, rvalue) {
return [ this[0], op, walk(lvalue), walk(rvalue) ];
},
"dot": function(expr) {
//expr.b 的AST是这样=> ["dot", expr, "b"];
return [ this[0], walk(expr) ].concat(slice(arguments, 1));
},
"call": function(expr, args) {
return [ this[0], walk(expr), MAP(args, walk) ];
},
"function": function(name, args, body) {
return [ this[0], name, args.slice(), MAP(body, walk) ];
},
"debugger": function() {
return [ this[0] ];
},
"defun": function(name, args, body) {
return [ this[0], name, args.slice(), MAP(body, walk) ];
},
"if": function(conditional, t, e) {
return [ this[0], walk(conditional), walk(t), walk(e) ];
},
"for": function(init, cond, step, block) {
return [ this[0], walk(init), walk(cond), walk(step), walk(block) ];
},
"for-in": function(vvar, key, hash, block) {
//for (var init in obj)
//AST为:["for-in", init, lhs, obj, statement]
return [ this[0], walk(vvar), walk(key), walk(hash), walk(block) ];
},
"while": function(cond, block) {
return [ this[0], walk(cond), walk(block) ];
},
"do": function(cond, block) {
return [ this[0], walk(cond), walk(block) ];
},
"return": function(expr) {
return [ this[0], walk(expr) ];
},
"binary": function(op, left, right) {
return [ this[0], op, walk(left), walk(right) ];
},
"unary-prefix": function(op, expr) {
return [ this[0], op, walk(expr) ];
},
"unary-postfix": function(op, expr) {
return [ this[0], op, walk(expr) ];
},
"sub": function(expr, subscript) {
//expr[subscript] 的AST是这样=> ["dot", expr, subscript];
return [ this[0], walk(expr), walk(subscript) ];
},
"object": function(props) {
return [ this[0], MAP(props, function(p){
return p.length == 2
? [ p[0], walk(p[1]) ]
//p[2] == get | set
//p[1] 是get|set的函数体
//p[0] 为get|set函数名
: [ p[0], walk(p[1]), p[2] ]; // get/set-ter
}) ];
},
"regexp": function(rx, mods) {
return [ this[0], rx, mods ];
},
"array": function(elements) {
return [ this[0], MAP(elements, walk) ];
},
"stat": function(stat) {
return [ this[0], walk(stat) ];
},
"seq": function() {
//逗号表达式
return [ this[0] ].concat(MAP(slice(arguments), walk));
},
"label": function(name, block) {//这里的block应该statement才对!
return [ this[0], name, walk(block) ];
},
"with": function(expr, block) {
return [ this[0], walk(expr), walk(block) ];
},
"atom": function(name) {
return [ this[0], name ];
},
"directive": function(dir) {
return [ this[0], dir ];
}
};
var user = {};//自定义树枝遍历器
var stack = [];//AST遍历时的堆栈信息
//遍历AST
function walk(ast) {
if (ast == null)
return null;
try {
//当前AST树压栈
stack.push(ast);
//["function", "func_name", arguments, body]
//AST树的第一个元素是这个树的类型
var type = ast[0];
//取出遍历钩子,这个钩子可以是外界传递进来 也可以是内部默认
//具体由with_walkers第一个参数来生成
var gen = user[type];
if (gen) {//如果有自定义的树枝遍历器,则用这个遍历器来遍历该树枝,得到结果
var ret = gen.apply(ast, ast.slice(1));
if (ret != null)
return ret;
}
//否则调用默认的树枝遍历器
gen = walkers[type];
return gen.apply(ast, ast.slice(1));
} finally {
//最后恢复堆栈信息
stack.pop();
}
};
//跟walk一样是遍历AST的功能,但是是采用默认的树枝遍历器来遍历
function dive(ast) {
if (ast == null)
return null;
try {
stack.push(ast);
return walkers[ast[0]].apply(ast, ast.slice(1));
} finally {
stack.pop();
}
};
//外边可以传入自定义的遍历器
//@param walkers 外界定义的遍历器
//@param cont @unknowed
function with_walkers(walkers, cont){
//walkers = {"function":function(){}}
var save = {}, i;
//i是语法规则名
for (i in walkers) if (HOP(walkers, i)) {
save[i] = user[i];//保存原来的遍历器
user[i] = walkers[i];//用新的遍历器覆盖之
}
var ret = cont();//执行钩子 一般这里外边会调用:walk(ast)来遍历AST树
//恢复原来的状态
for (i in save) if (HOP(save, i)) {
if (!save[i]) delete user[i];
else user[i] = save[i];
}
//得到遍历后生成的新的AST树
return ret;
};
return {
walk: walk,
dive: dive,
with_walkers: with_walkers,
parent: function() {
//如果是当前这样的AST树
//["toplevel", ["stat", ["function", "A", [], []]]]
//遍历到树枝function的时候
//stack是这样的
/*
["toplevel", ["stat", ["function", "A", [], []]]]
["stat", ["function", "A", [], []]]
["function", "A", [], []]
*/
//function的父亲其实就是堆栈stack的倒数第二个节点
return stack[stack.length - 2]; // last one is current node
},
stack: function() {
return stack;
}
};
};
作用域信息
/*
这份代码设计了作用域链的属性以及方法
同时还有一个遍历AST树给节点加作用域信息的with_new_scope方法
*/
//作用域信息类
function Scope(parent) {
//当前作用域的符号表,包括变量跟函数变量
this.names = {}; // names defined in this scope
//混淆变量表
//例如源代码是:var myOldName; 压缩后变成:var e;
//那么mangled跟rev_mangled分别记录着这个映射关系
//mangled["myOldName"] = "e" | rev_mangled["e"] = "myOldName"
this.mangled = {}; // mangled names (orig.name => mangled)
this.rev_mangled = {}; // reverse lookup (mangled => orig.name)
//当前作用域已经混淆的变量个数
this.cname = -1; // current mangled name
//当前作用域使用到的引用变量名字
//例如 function(){var i, j; j = 1;}
//此时refs = {"j" : }; i只是一个声明 不是一个引用
this.refs = {}; // names referenced from this scope
//如果在with里边?@unkowned
this.uses_with = false; // will become TRUE if with() is detected in this or any subscopes
//如果在eval里边?@unkowned
this.uses_eval = false; // will become TRUE if eval() is detected in this or any subscopes
//作用域的指示性字符串列表,例如:"use strict";
this.directives = []; // directives activated from this scope
//当前作用域的父亲作用域,由此可以搞成一个作用域链!
this.parent = parent; // parent scope
//当前作用于的子作用域列表
this.children = []; // sub-scopes
//如果设置了父亲,那么在父亲的children加上当前对象。
//level只嵌套深度
if (parent) {
this.level = parent.level + 1;
parent.children.push(this);
} else {
this.level = 0;
}
};
function base54_digits() {
//你可以自定义混淆的表哦~通过自己定义DIGITS_OVERRIDE_FOR_TESTING这个变量
if (typeof DIGITS_OVERRIDE_FOR_TESTING != "undefined")
return DIGITS_OVERRIDE_FOR_TESTING;
else
//为什么是下边这个字符串?用这个顺序混淆之后 再gzip之后会得到更少的字节
//这里要了解gzip算法 @unkowned
//见:https://github.com/mishoo/UglifyJS/commit/4072f80ada49f8bd541045690f5f922ff5a43b59
//Optimize list of digits for generating identifiers for gzip compression.
//The list is based on reserved words and identifiers used in dot-expressions. It saves a quite a few bytes.
return "etnrisouaflchpdvmgybwESxTNCkLAOM_DPHBjFIqRUzWXV$JKQGYZ0516372984";
}
var base54 = (function(){
var DIGITS = base54_digits();
//最后得到的混淆顺序是这样:
//e t n r …… Z
//et tt nt rt …… Zt //为什么这里不是 ee te ……
//……
return function(num) {
/*
//为了第二位数也是从e开始: ee te ……
//其实可以优化成这个样子:
var ret = "", base = 54;//54是前边54个英文+$ 因为不能用数字开头
var b = 0, maxb = 1 + num > 54 ? Math.ceil((num-54+1)/64) : 0;
do {
ret += DIGITS.charAt(num % base);
b++;
num = Math.floor(num / base) - 1;
base = 64;
} while (num >= 0 && b 0);
return ret;
};
})();
//作用域对象成员方法
Scope.prototype = {
//判断在当前作用域上能不能找到变量name
has: function(name) {
//沿着作用链一层一层搜索符号表 有木有!
for (var s = this; s; s = s.parent)
if (HOP(s.names, name))
return s;
},
//看看当前混淆的名字处于那个作用链上边
has_mangled: function(mname) {
for (var s = this; s; s = s.parent)
if (HOP(s.rev_mangled, mname))
return s;
},
//这个没太大意义
toJSON: function() {
return {
names: this.names,
uses_eval: this.uses_eval,
uses_with: this.uses_with
};
},
//这个函数就是变量名字混淆的关键了!
next_mangled: function() {
// we must be careful that the new mangled name:
//
// 1. doesn‘t shadow a mangled name from a parent
// scope, unless we don‘t reference the original
// name from this scope OR from any sub-scopes!
// This will get slow.
//
// 2. doesn‘t shadow an original name from a parent
// scope, in the event that the name is not mangled
// in the parent scope and we reference that name
// here OR IN ANY SUBSCOPES!
//
// 3. doesn‘t shadow a name that is referenced but not
// defined (possibly global defined elsewhere).
for (;;) {
//留意了,通过base54这个函数生成混淆后的名字
var m = base54(++this.cname), prior;
//有个优先级
// case 1.
/*
对应这样的情况
var name = {};//混淆后得到变量名字a
function(){
//在这里边要混淆name2这个变量成名字a 发现a已经在父亲作用域时混淆的时候用到了
//prior = this.has_mangled("a"); => 父亲作用域
//那就得看看在当前作用域内,name有没有被引用了
//如果有name.b = 1 那么name2就不能用名字a
//否则可以使用名字a
var name2 = {};
name.b = 1;
}
*/
prior = this.has_mangled(m);
if (prior && this.refs[prior.rev_mangled[m]] === prior)
continue;
// case 2.
/*
对应这样的情况
e = {};//这个在父亲作用域有e这个变量
function(){
//this 这里想要把变量name1也混淆成e这个名字
var name1;
e.a = 1;
}
*/
prior = this.has(m);
//!prior.has_mangled(m)说明了e这个变量名字不是混淆 而是原始名字 这里可以认为是全局作用域的引用!
if (prior && prior !== this && this.refs[m] === prior && !prior.has_mangled(m))
continue;
// case 3.
/*
对应这样的情况
name = 1;
//这种name是全局对象,通过refs[m]找不到对应的作用域,这种变量名字也不能混淆!
*/
if (HOP(this.refs, m) && this.refs[m] == null)
continue;
// I got "do" once. :-/
if (!is_identifier(m))
continue;
return m;
}
},
//设置混淆变量名的符号表而已
set_mangle: function(name, m) {
this.rev_mangled[m] = name;
return this.mangled[name] = m;
},
//获取name变量映射的混淆名
get_mangled: function(name, newMangle) {
//在with跟eval里边不混淆变量名!
if (this.uses_eval || this.uses_with) return name; // no mangle if eval or with is in use
var s = this.has(name);
//不在作用域链上的 可能是别的文件定义的全局变量 所以不能混淆!
if (!s) return name; // not in visible scope, no mangle
//已经混淆过的,那直接返回即可
if (HOP(s.mangled, name)) return s.mangled[name]; // already mangled in this scope
//外部调用指定newMangle = false告诉你不混淆 还混淆个毛线~
if (!newMangle) return name; // not found and no mangling requested
//最后如果发现需要混淆了,那么调用next_mangled得到一个混淆名 同时设置好符号表映射关系
return s.set_mangle(name, s.next_mangled());
},
//看看name是不是一个引用,以下几个情况都属于引用:
//在全局域里边的name
//在with eval里边的变量都属于引用,名字不能混淆
//或者当前作用域有refs[name]
references: function(name) {
return name && !this.parent || this.uses_with || this.uses_eval || this.refs[name];
},
//记录当前作用域的变量声明
define: function(name, type) {
if (name != null) {
if (type == "var" || !HOP(this.names, name))
this.names[name] = type || "var";
return name;
}
},
//@unkowned
active_directive: function(dir) {
return member(dir, this.directives) || this.parent && this.parent.active_directive(dir);
}
};
//为当前AST树加入作用域信息
function ast_add_scope(ast) {
var current_scope = null;
var w = ast_walker(), walk = w.walk;
var having_eval = [];
function with_new_scope(cont) {
//为当前生成一个子作用域,加入到作用域链中
current_scope = new Scope(current_scope);
current_scope.labels = new Scope();
//拿到作用域块的AST树枝
var ret = current_scope.body = cont();
//把作用域信息记录在树枝上
ret.scope = current_scope;
//回到上一层作用域!
current_scope = current_scope.parent;
return ret;
};
function define(name, type) {
return current_scope.define(name, type);
};
function reference(name) {
current_scope.refs[name] = true;
};
function _lambda(name, args, body) {
var is_defun = this[0] == "defun";
return [ this[0], is_defun ? define(name, "defun") : name, args, with_new_scope(function(){
//进入函数体 要生成一个作用域信息
if (!is_defun) define(name, "lambda");
//当前函数声明的参数为此作用域的符号信息
MAP(args, function(name){ define(name, "arg") });
return MAP(body, walk);
})];
};
function _vardefs(type) {
return function(defs) {
//var a = b;
//b要算进引用列表~
//a要算进声明列表
MAP(defs, function(d){
define(d[0], type);
if (d[1]) reference(d[0]);
});
};
};
function _breacont(label) {
if (label)
current_scope.labels.refs[label] = true;
};
return with_new_scope(function(){
// process AST
var ret = w.with_walkers({
"function": _lambda,
"defun": _lambda,
"label": function(name, stat) { current_scope.labels.define(name) },
"break": _breacont,
"continue": _breacont,
"with": function(expr, block) {
for (var s = current_scope; s; s = s.parent)
s.uses_with = true;
},
"var": _vardefs("var"),
"const": _vardefs("const"),
"try": function(t, c, f) {
if (c != null) return [
this[0],
MAP(t, walk),
[ define(c[0], "catch"), MAP(c[1], walk) ],
f != null ? MAP(f, walk) : null
];
},
"name": function(name) {
if (name == "eval")
having_eval.push(current_scope);
//留意一下
//var a = 1; 这里的a不是引用 只是一个声明
//只有在真正使用a的时候才算是引用,例如:
//a.b; a=1
//留意:for (var a in arr) 这里的a也算是一个引用,因为相当于var a;for(a in arr)
reference(name);//记录一下当前作用域使用到的引用
}
}, function(){
return walk(ast);
});
// the reason why we need an additional pass here is
// that names can be used prior to their definition.
// scopes where eval was detected and their parents
// are marked with uses_eval, unless they define the
// "eval" name.
//如果某个作用域有使用eval,会导致这条作用域链上边的变量都不能混淆
MAP(having_eval, function(scope){
if (!scope.has("eval")) while (scope) {
scope.uses_eval = true;
scope = scope.parent;
}
});
// for referenced names it might be useful to know
// their origin scope. current_scope here is the
// toplevel one.
//本来 refs = {a:true} 现在要fix成 refs = {a:}
//需要知道变量a在哪个作用域上被引用,因为这会影响变量名混淆的操作
function fixrefs(scope, i) {
// do children first; order shouldn‘t matter
for (i = scope.children.length; --i >= 0;)
fixrefs(scope.children[i]);
for (i in scope.refs) if (HOP(scope.refs, i)) {
// find origin scope and propagate the reference to origin
//找到当前引用变量名字i在哪个作用域声明的!
//
/*
var a = 1; //当前作用域 => origin | s.parent.parent
(functin (){ //当前作用域 => s.parent
(function(){ //当前作用域 => s
a = 1;
})
})
由于在s.parent.parent 以及 s.parent是不知道a被s引用了
所以这里要从底层递归上来 记录每一个作用域都引用了a 防止a变量在中间某层被认作是一个没用的变量干掉了。
*/
for (var origin = scope.has(i), s = scope; s; s = s.parent) {
s.refs[i] = origin;
if (s === origin) break;
}
}
};
//修复当前作用域的引用
fixrefs(current_scope);
return ret;
});
}
变量混淆
//混淆变量名需要在静态分析时候知道当前作用域链
//ast_mangle执行前需要先执行ast_add_scope,把作用域信息记录在树枝上
function ast_mangle(ast, options) {
//拿到一个遍历器
var w = ast_walker(), walk = w.walk, scope;
options = defaults(options, {
mangle : true,
toplevel : false,
defines : null,
except : null,
no_functions : false
});
//关键函数
//输入变量名字name 输出混淆后的变量名
function get_mangled(name, newMangle) {
//如果参数指定不混淆变量名 那还做啥!
if (!options.mangle) return name;
//如果参数指定不混淆全局变量 并且当前作用域是在全局上 那还做啥!
if (!options.toplevel && !scope.parent) return name; // don‘t mangle toplevel
//你可以为uglify指定一些不要他混淆的变量名
if (options.except && member(name, options.except))
return name;
//参数指定不混淆函数变量名:uglify --no-mangle-functions
//defun 指的是定义的函数,语句块以这样开始:function A(){}
//留意 var c = function A(){}这种不算defun
if (options.no_functions && HOP(scope.names, name) &&
(scope.names[name] == ‘defun‘ || scope.names[name] == ‘lambda‘))
return name;
//除了上边不用混淆的情况,其他情况都要混淆
//具体混淆算法见scope.get_mangled
return scope.get_mangled(name, newMangle);
};
//可以自己为某些变量做变量名替换的操作,例如:
//uglifyjs -o a.js a.js -c --define DEBUG=true
//那么代码中的DEBUG变量最后会被替换成true
function get_define(name) {
if (options.defines) {
// we always lookup a defined symbol for the current scope FIRST, so declared
// vars trump a DEFINE symbol, but if no such var is found, then match a DEFINE value
if (!scope.has(name)) {//留意这个判断!如果当前作用域没有这个变量 才会考虑用参数里边的列表映射替换这个变量!
if (HOP(options.defines, name)) {
return options.defines[name];
}
}
return null;
}
};
function _lambda(name, args, body) {
if (!options.no_functions && options.mangle) {//如果函数名需要混淆!
var is_defun = this[0] == "defun", extra;
if (name) {
//如果是函数定义 那么名字要混淆
if (is_defun) name = get_mangled(name);
//如果是这样的情况:
//(function A(){})(); A函数里边没有引用自己 所以等同于 (function(){})();
//(function A(){A();}) A函数递归自己,那么A这个名字就要参与混淆了
else if (body.scope.references(name)) {
extra = {};//混淆后的名字要记录起来 函数体里边的作用域就不能再用这个名字了
//当前作用域没有使用with以及eval的情况才能混淆名字
if (!(scope.uses_eval || scope.uses_with))
name = extra[name] = scope.next_mangled();
else
extra[name] = name;
}
else name = null;
}
}
//函数体要在其作用域去混淆变量名
body = with_scope(body.scope, function(){
//函数参数名需要混淆
args = MAP(args, function(name){ return get_mangled(name) });
return MAP(body, walk);
}, extra);
return [ this[0], name, args, body ];
};
function with_scope(s, cont, extra) {
var _scope = scope;
scope = s;
//extra表示当前作用域已经使用过的混淆名字
if (extra) for (var i in extra) if (HOP(extra, i)) {
s.set_mangle(i, extra[i]);
}
for (var i in s.names) if (HOP(s.names, i)) {
get_mangled(i, true);//为当前作用域使用到的名字做混淆
}
var ret = cont();
ret.scope = s;//绑定作用域信息
scope = _scope;
return ret;
};
function _vardefs(defs) {//变量名字混淆
return [ this[0], MAP(defs, function(d){
return [ get_mangled(d[0]), walk(d[1]) ];
}) ];
};
function _breacont(label) {//label标签的混淆!
if (label) return [ this[0], scope.labels.get_mangled(label) ];
};
//自定义其中一些涉及到需要混淆变量名的树枝遍历器
return w.with_walkers({
"function": _lambda,
"defun": function() {
// move function declarations to the top when
// they are not in some block.
//先混淆函数名以及函数体的变量名字
//得到一个新的树枝
var ast = _lambda.apply(this, arguments);
//看看要不要把当前定义提到最前边
/*
var a = 1,b = 2;
function C(){}
function D(){}
其实可以优化成:
function C(){}
function D(){}
var a = 1,b = 2;
*/
switch (w.parent()[0]) {
case "toplevel":
case "function":
case "defun":
//把函数定义提前
return MAP.at_top(ast);
}
return ast;
},
"label": function(label, stat) {
if (scope.labels.refs[label]) return [
this[0],
//获取label对应的混淆名字
scope.labels.get_mangled(label, true),
walk(stat)
];
//如果没有一个地方引用当前label 那可以去掉这个label了
return walk(stat);
},
"break": _breacont,
"continue": _breacont,
"var": _vardefs,
"const": _vardefs,
"name": function(name) {
//看看当前名字有没有在作用域链声明 有的话才混淆
return get_define(name) || [ this[0], get_mangled(name) ];
},
"try": function(t, c, f) {
return [ this[0],
MAP(t, walk),
c != null ? [ get_mangled(c[0]), MAP(c[1], walk) ] : null,
f != null ? MAP(f, walk) : null ];
},
"toplevel": function(body) {
var self = this;//为什么这里会有self.scope,因为在ast_add_scope已经为树生成了作用域信息
return with_scope(self.scope, function(){
return [ self[0], MAP(body, walk) ];
});
},
"directive": function() {
//指示性字符串也提到当前作用域前边
//function(){var a = 1; "use strict";}
//优化成 function(){"use strict"; var a = 1;}
return MAP.at_top(this);
}
}, function() {
//混淆变量名字需要绑定节点的作用域信息!
return walk(ast_add_scope(ast));
});
}
//辅助方法
var MAP;
(function(){
//遍历一个语句块a的时候
MAP = function(a, f, o) {
//可能有函数定义 以及 指示性字符串放到这个块最前边
//所以top就记录了这些树枝
var ret = [], top = [], i;
function doit() {
//遍历的过程 把AtTop的类型提到top数组
var val = f.call(o, a[i], i);
if (val instanceof AtTop) {
val = val.v;
if (val instanceof Splice) {
top.push.apply(top, val.v);
} else {
top.push(val);
}
}
//其余的 看看语句是否可以忽略 不能忽略的语句放到ret数组
else if (val != skip) {
if (val instanceof Splice) {
ret.push.apply(ret, val.v);
} else {
ret.push(val);
}
}
};
if (a instanceof Array) for (i = 0; i < a.length; ++i) doit();
else for (i in a) if (HOP(a, i)) doit();
//top数组一定排在ret数组之前
return top.concat(ret);
};
MAP.at_top = function(val) { return new AtTop(val) };
MAP.splice = function(val) { return new Splice(val) };
var skip = MAP.skip = {};
function AtTop(val) { this.v = val };
function Splice(val) { this.v = val };
})();