Javascript变量名混淆细节

前言

UglifyJS会对JS文件的变量名进行混淆处理。要理解Javascript变量混淆的细节。我们须要回答下面几个问题:
1.遇到一个变量myName,我们怎么知道这个myName变量要不要混淆
2.混淆名字怎么生成才合适。新的名字替换旧的名字时有什么要注意的地方?
3.哪些keyword会产生一个作用域?
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:当前作用域不同变量混淆后的变量名不能反复。同一时候混淆后的名字不能是keyword。

其次要考虑下面几种在作用域链上的特殊情况:

场景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.哪些keyword会产生一个作用域?

在UglifyJS里边採用with_new_scope这个函数来为AST树枝生成作用域信息。


能够从其源代码中看到两个地方会产生作用域信息。


规则3.1:整个JS文件处于一个全局作用域,UglifyJS称为toplevel。
规则3.2:functionkeyword声明的函数内部处于一个作用域。


规则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的声明。
可是同withkeyword一样。UglifyJS没法静态分析得到这个信息,因此它不会为catch这个AST树枝生成作用域信息。

可是!catch跟with不同的地方就是。catch在静态分析的时候是能够知道在catch块作用域里边声明的变量了,没错,就是catch后边带的參数名字e,因此UglifyJS是会对这个变量进行混淆处理的。

4.作用域链跟符号表在UglifyJS里边是怎么体现?

UglifyJS的实现里边是使用Scope作为作用域类,这边罗列一下其属性。具体的实现见文章末尾的代码凝视:

  1. names = {} 表示在当前作用域下声明的变量函数的名字,也即是我们经常说的符号表!
  2. mangled = {} 混淆前变量以及混淆后变量的映射表,变量myName混淆后得到变量e,那么mangled["myName"] == "e"。
  3. rev_mangled = {} 跟mangled反过来,为了能够反查出混淆前变量的名字是什么。

  4. cname =
  5. refs = {} 记录着当前作用域内引用的信息,ref["a"] = <Scope B>
  6. 表示当前作用域使用了作用域链上B作用域所声明的a变量。

  7. uses_with = true/false 表示当前作用域有没有使用了with块,假设有的话,这条作用域链上的变量都不能混淆。

  8. uses_eval = true/false 表示当前作用域有没有使用eval函数。假设有的话。这条作用域链上的变量都不能混淆。
  9. parent = <Scope> 父亲作用域是谁。

  10. children = [<Scope>] 孩子作用域列表。
  11. 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 };
})();
posted @ 2017-04-19 20:33  lytwajue  阅读(3152)  评论(0编辑  收藏  举报