uglifyjs是用来压缩混淆JS文件,同时会对代码尽可能的优化,使得最后产出的js代码非常小,uglifyjs通过nodejs写的,底层用了一个parse-js的进行AST分析。
这两天在工作空隙的时间阅读了uglifyjs的源码,发现了很多js语法细节的东西,略有收获,顺便把看的过程注释在它代码里边。
词法分析的文章写过好几篇了,不想重复了,直接贴代码注释了。
javascript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 | //词法分析 by raphealguo function tokenizer($TEXT) { var S = { //把空格 空白标准化处理掉 text : $TEXT.replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, ''), //当前扫描到第pos个字符 pos : 0, //当前token所在位置 tokpos : 0, //当前扫描到第line行 line : 0, //当前token所在行 tokline : 0, //当前扫描到第col列 col : 0, //当前token所在列 tokcol : 0, //当前扫描的token字符串中包含了换行 newline_before : false, //在某些符号或者关键字后边才能出现正则表达式! //需要有标志位标记是否读取正则的token regex_allowed : false, //当前token前边的注释 comments_before : [] }; //获取当前扫描位置的字符 function peek() { return S.text.charAt(S.pos); }; //得到下一个字符 //signal_eof表示要不要检测EOF //in_string表示当前扫描的字符是否在 "" 或者 '' 里边 function next(signal_eof, in_string) { var ch = S.text.charAt(S.pos++); if (signal_eof && !ch) throw EX_EOF; if (ch == "\n") {//扫描到换行符 //当前扫描的token字符串中包含了换行 S.newline_before = S.newline_before || !in_string; //重新计算所在行跟列 ++S.line; S.col = 0; } else { ++S.col; } return ch; }; //是否到文件末尾 function eof() { return !S.peek(); }; //辅助函数 从当前文本扫描的位置后找what出现的位置 function find(what, signal_eof) { var pos = S.text.indexOf(what, S.pos); if (signal_eof && pos == -1) throw EX_EOF; return pos; }; //开始扫描前记录 function start_token() { S.tokline = S.line; S.tokcol = S.col; S.tokpos = S.pos; }; //得到一个token对象 function token(type, value, is_comment) { //在以下这些符号或者关键字后边才能出现正则表达式! //var UNARY_POSTFIX = array_to_hash([ "--", "++" ]); //var KEYWORDS_BEFORE_EXPRESSION = array_to_hash(["return","new","delete","throw","else","case"]); //var PUNC_BEFORE_EXPRESSION = array_to_hash(characters("[{(,.;:")); S.regex_allowed = ((type == "operator" && !HOP(UNARY_POSTFIX, value)) || (type == "keyword" && HOP(KEYWORDS_BEFORE_EXPRESSION, value)) || (type == "punc" && HOP(PUNC_BEFORE_EXPRESSION, value))); //一个token带有如下信息 var ret = { type : type,//token类型 value : value,//token对应的值 //token对应的源码位置信息 line : S.tokline, col : S.tokcol, pos : S.tokpos, endpos : S.pos, nlb : S.newline_before }; if (!is_comment) {//如果不是注释 //扫描当前token前的注释都属于当前的token ret.comments_before = S.comments_before; S.comments_before = []; // make note of any newlines in the comments that came before //看看在前边一个token跟当前token中间有没有空行 for (var i = 0, len = ret.comments_before.length; i < len; i++) { ret.nlb = ret.nlb || ret.comments_before[i].nlb; } } S.newline_before = false; return ret; }; //忽略空白符,直到当前去到的字符不是空白 function skip_whitespace() { while (HOP(WHITESPACE_CHARS, peek())) next(); }; //辅助函数,用来读出token对应的字符串 //例如扫描到 1.0002的1的时候,应该把1.0002整个字符串都认为是一个num的token //直到pred回调为false之后 function read_while(pred) { var ret = "", ch = peek(), i = 0; while (ch && pred(ch, i++)) { ret += next(); ch = peek(); } return ret; }; //辅助函数 用于抛出错误 function parse_error(err) { js_error(err, S.tokline, S.tokcol, S.tokpos); }; //数字状态机 //prefix 前缀,例如+-. function read_num(prefix) { var has_e = false, //是否包含E 科学计数法 after_e = false, //是否在E后边,科学计数法 has_x = false, //是否有X 十六进制 has_dot = prefix == ".";//是否小数 带. //开始读出数字的token,直到回调返回false var num = read_while(function(ch, i){ if (ch == "x" || ch == "X") { if (has_x) return false;//当前如果是X/x 如果 return has_x = true;//继续下个字符 } //科学计数法不能有X if (!has_x && (ch == "E" || ch == "e")) { if (has_e) return false; return has_e = after_e = true; } //如果有-号 必须要在E后边 或者前边没东西:-1 if (ch == "-") { if (after_e || (i == 0 && !prefix)) return true; return false; } //+号必须出现在E后边 if (ch == "+") return after_e; after_e = false; if (ch == ".") {//有.的不能再出现. X E if (!has_dot && !has_x && !has_e) return has_dot = true; return false; } //如果是数字或者字符就继续扫描 return is_alphanumeric_char(ch); }); //有前缀加上前缀 if (prefix) num = prefix + num; /* function parse_js_number(num) { if (RE_HEX_NUMBER.test(num)) { return parseInt(num.substr(2), 16); } else if (RE_OCT_NUMBER.test(num)) { return parseInt(num.substr(1), 8); } else if (RE_DEC_NUMBER.test(num)) { return parseFloat(num); } }; */ //parse_js_number 会检查是否16 8 10进制 var valid = parse_js_number(num); //合法的数字 就返回num的token if (!isNaN(valid)) { return token("num", valid); } else {//否则词法错误 parse_error("Invalid syntax: " + num); } }; //读取转义字符 function read_escaped_char(in_string) { var ch = next(true, in_string);//转义字符一般是在字符串里边 所以in_string一般是true switch (ch) { case "n" : return "\n"; case "r" : return "\r"; case "t" : return "\t"; case "b" : return "\b"; case "v" : return "\u000b"; case "f" : return "\f"; case "0" : return "\0"; case "x" : return String.fromCharCode(hex_bytes(2));//\x后边两个字符 case "u" : return String.fromCharCode(hex_bytes(4));//\u后边有4个字符 case "\n": return "";//如果语句的最后跟一个\表示要跟下一行连在一起 所以返回 "" default : return ch;//其他不是转义字符的 否则"\A" == "A" } }; //读取n位16进制的位。例如\uA013中的A013 function hex_bytes(n) { var num = 0; for (; n > 0; --n) { var digit = parseInt(next(true), 16); if (isNaN(digit)) parse_error("Invalid hex-character pattern in string"); num = (num << 4) | digit; } return num; }; //字符串状态机 function read_string() { return with_eof_error("Unterminated string constant", function(){ var quote = next(), ret = ""; //quote = next();先吃掉一个"或者' for (;;) { var ch = next(true);//next的第一个参数表示了当前到EOF要报错 if (ch == "\\") {//遇到\要吃掉后边的字符,例如\n \t \r \123 // read OctalEscapeSequence (XXX: deprecated if "strict mode") // https://github.com/mishoo/UglifyJS/issues/178 var octal_len = 0, first = null; //下边是为了读取\123这样的八进制字符 ch = read_while(function(ch){ if (ch >= "0" && ch <= "7") {//八进制的数肯定在0~7中间 if (!first) { first = ch; return ++octal_len; } //第一位小于等于3至多只能有3位的长度 // 0377 十进制就是255了 else if (first <= "3" && octal_len <= 2) return ++octal_len; //第一位>=4的 至多只能有2为长度 // 040 == 32 077 = 63 else if (first >= "4" && octal_len <= 1) return ++octal_len; } return false; }); if (octal_len > 0){ //如果八进制的字符 // \123其实表示acsii中83(123是八进制)的字符S ch = String.fromCharCode(parseInt(ch, 8)); }else{ //否则就是\n \t这样的转义字符 ch = read_escaped_char(true);//true表示in_string } } else if (ch == quote) break;//读到"/'就结束了 else if (ch == "\n") throw EX_EOF;//字符串里边不能回车 ret += ch;//连接字符串 } return token("string", ret); }); }; //读取单行注释 function read_line_comment() { next();//吃掉当前字符 var i = find("\n"), ret; if (i == -1) {//到文件末尾都还没找到换行符 ret = S.text.substr(S.pos); S.pos = S.text.length; } else { ret = S.text.substring(S.pos, i); S.pos = i; } return token("comment1", ret, true); }; //读取多行注释 function read_multiline_comment() { next(); return with_eof_error("Unterminated multiline comment", function(){ var i = find("*/", true), text = S.text.substring(S.pos, i); S.pos = i + 2; S.line += text.split("\n").length - 1; S.newline_before = S.newline_before || text.indexOf("\n") >= 0; // https://github.com/mishoo/UglifyJS/issues/#issue/100 if (/^@cc_on/i.test(text)) { warn("WARNING: at line " + S.line); warn("*** Found \"conditional comment\": " + text); warn("*** UglifyJS DISCARDS ALL COMMENTS. This means your code might no longer work properly in Internet Explorer."); } return token("comment2", text, true); }); }; //返回一个串 可能是变量名字 可能是关键字 可能是操作符 还有可能是true false null undefined function read_name() { var backslash = false, name = "", ch, escaped = false, hex; while ((ch = peek()) != null) { if (!backslash) { if (ch == "\\") escaped = backslash = true, next(); else if (is_identifier_char(ch)) name += next(); else break; } else {//如果前边是\ 那后边需要带u 必须UnicodeEscape形式 if (ch != "u") parse_error("Expecting UnicodeEscape形式Sequence -- uXXXX"); ch = read_escaped_char();//读出这个字符 //看看是不是合法字符 if (!is_identifier_char(ch)) parse_error("Unicode char: " + ch.charCodeAt(0) + " is not valid in identifier"); name += ch; backslash = false; } } if (HOP(KEYWORDS, name) && escaped) {//如果串中带有\ 那必须不能是关键字 hex = name.charCodeAt(0).toString(16).toUpperCase(); name = "\\u" + "0000".substr(hex.length) + hex + name.slice(1);//所以这里特意对第一个字符变成\uxxxx的形式 } return name; }; //读取正则表达式 function read_regexp(regexp) { return with_eof_error("Unterminated regular expression", function(){ var prev_backslash = false, ch, in_class = false; while ((ch = next(true))) if (prev_backslash) {//前边有\ regexp += "\\" + ch; prev_backslash = false; } else if (ch == "[") {//var reg = /[/]/ 允许这样! in_class = true; regexp += ch; } else if (ch == "]" && in_class) { in_class = false; regexp += ch; } else if (ch == "/" && !in_class) { break; } else if (ch == "\\") { prev_backslash = true; } else { regexp += ch; } //var reg = /xxx/gi 最后还要读一个gi这个word var mods = read_name(); return token("regexp", [ regexp, mods ]); }); }; //读取操作符号 例如 in instanceof new ++ -- <<= === //尽可能多的向后看 组成最可能的操作符 //操作符有: /* var OPERATORS = array_to_hash(["in", "instanceof", "typeof", "new", "void", "delete", "++", "--", "+", "-", "!", "~", "&", "|", "^", "*", "/", "%", ">>", "<<", ">>>", "<", ">", "<=", ">=", "==", "===", "!=", "!==", "?", "=", "+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&=", "&&", "||"]); */ function read_operator(prefix) { function grow(op) { if (!peek()) return op;//如果到末尾 就返回当前的操作符 var bigger = op + peek();//否则向后看多一个字符 //如果在OPERATORS列表里边 if (HOP(OPERATORS, bigger)) { next();//吃掉当前向后看的字符 //继续往后找合适的操作符 return grow(bigger); } else { return op; } }; return token("operator", grow(prefix || next())); }; //解析/ 可能是注释 可能是除法 /=?可能是正则表达式 function handle_slash() { next();//吃掉一个/字符 //看看当前位置允许正则不 var regex_allowed = S.regex_allowed; switch (peek()) { case "/"://单行注释 S.comments_before.push(read_line_comment()); S.regex_allowed = regex_allowed;//为什么要赋值多一次? 在read_line_comment里边调用了token(), token一开始可能会改变S.regex_allowed //注释的token不会参与语法解析,所以取下个token返回 return next_token(); case "*"://多行注释 S.comments_before.push(read_multiline_comment()); S.regex_allowed = regex_allowed;//为什么要赋值多一次? 在read_multiline_comment里边调用了token(), token一开始可能会改变S.regex_allowed //注释的token不会参与语法解析,所以取下个token返回 return next_token(); } //要么是正则 要么是除号 还得看看是不是/=这种操作符 return S.regex_allowed ? read_regexp("") : read_operator("/"); }; //解析.状态 function handle_dot() { next();//先吃掉. //接着看看当前位置是不是数字,如果是的话 进入read_num读取一个数字token //否则 得到一个.的token ,为语法解析属性做准备 return is_digit(peek()) ? read_num(".") : token("punc", "."); }; //读一个串 看看是什么token function read_word() { var word = read_name();//读取一个串 return !HOP(KEYWORDS, word) ? token("name", word)//如果不是关键字 那么就是一个变量名 : HOP(OPERATORS, word) ? token("operator", word)//操作符 : HOP(KEYWORDS_ATOM, word) ? token("atom", word)//如果是true false null undefined : token("keyword", word);//关键字 }; //辅助函数 //eof_error EOF的错误抛出信息 function with_eof_error(eof_error, cont) { try { return cont(); } catch(ex) { if (ex === EX_EOF) parse_error(eof_error); else throw ex; } }; //获取下一个token function next_token(force_regexp) { //force_regexp 还不知道什么意思 if (force_regexp != null) return read_regexp(force_regexp); //忽略空白符 skip_whitespace(); //初始换扫描token触发 start_token(); //得到当前第一个字符 var ch = peek(); //状态机来了! //遇到文件末尾 则EOF if (!ch) return token("eof"); //parse-js的读取某个状态都是用read_开头,返回是一个token对象,如下: //如果ch是数字,则进入读数字的状态机 if (is_digit(ch)) return read_num(); //进入字符串状态 if (ch == '"' || ch == "'") return read_string(); //var PUNC_CHARS = array_to_hash(characters("[]{}(),;:")); //punc char 表示标点字符 直接得到一个token 值就是这个字符即可 if (HOP(PUNC_CHARS, ch)) return token("punc", next()); //解析. if (ch == ".") return handle_dot(); //解析/ 可能是注释 可能是除法?可能是正则表达式 //如果是注释的话 是不会作为token返回,在handle_slash里边会再去取next_token来返回 if (ch == "/") return handle_slash(); //操作符状态 //var OPERATOR_CHARS = array_to_hash(characters("+-*&%=<>!?|~^")); if (HOP(OPERATOR_CHARS, ch)) return read_operator(); /* function is_identifier_start(ch) { return ch == "$" || ch == "_" || is_letter(ch); }; */ //如果是\开头 或者是 $_数字开头 可以认为是一个变量之类的~ //可以命名变量 var \u1234 = 1; => window["ሴ"] = 1 if (ch == "\\" || is_identifier_start(ch)) return read_word(); //遇到其他字符都跑出错无 parse_error("Unexpected character '" + ch + "'"); }; //设置token的上下文 next_token.context = function(nc) { if (nc) S = nc; return S; }; //返回一个获取下一个token的钩子 return next_token; } |
转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!
mark
应该发上km
已发。