前言
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作為作用域類,這邊羅列一下其屬性。具體的實現見文章末尾的代碼凝視:
- 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 };
})();