前言
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这个全局变量