本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考
& X `6 A }! G3 M& T; D: l$ }( p" R CC 是真正的編譯器1 Q1 `% s0 m& L5 @- \+ b
Closure Compiler 和 YUICompressor 並不是同類產品,雖然 CC 和 YC 同樣產出壓縮後的 JS 文件,但是 YC 只做了詞法上的掃瞄,而 CC 並不只是一個 compressor 那麼簡單,器如其名,它是一個compiler。1 B+ s* x( U/ f1 E, S; I7 h" S
對於一個 compiler,一般地,它需要做到:
3 F0 F/ h$ R/ U9 b檢查源文本中語法、語義、語用上的錯誤;
9 t+ u9 b! N, \, A% X3 q- i' B根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;
5 v, ^5 }2 o# n% W- k4 }優化。( {" j3 C- d& S! e: |+ p
代碼錯誤一般來自三個方面:/ T$ _$ P/ }. l5 }6 A: M
語法(Syntax)
9 F2 T) L; l5 U9 n/ ?' y表示構成語言句子的各個記號之間的組合規律。大體上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……
1 Q% q) o. h4 d7 h語法上的錯誤,如:
0 B7 o2 s2 |* b0 W" M( }doSomething(;) // SyntaxError: Unexpected token ; @0 i6 M4 h3 d
根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並沒有配對),因此報錯。
, T4 i# ]: l; P5 |語義(Semantics)4 f% v, D* S5 ^; u! \& x7 F! \
表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 需要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。
* P! D/ Y" a. p/ a2 Q語義上的錯誤,如:
3 v% y* c9 W. O0 = {}; // ReferenceError: Invalid left-hand side in assignment
. t4 q. T3 o' a; M根據賦值運算符 = 的意義,左操作數不能為字面量,所以雖然這個賦值語句包含了必需的左操作數、運算符、右操作數,仍然出錯。
h" V- o( o/ V語用(Pragmatics)9 S; W" p2 S9 g9 Q6 u) s
表示在各個記號所出現的行為中,它們的來源、使用和影響。
( Z. j2 G$ p8 t' g4 ~語用上的錯誤,如:* n9 Q: H. m* O$ d
doSomething(); // ReferenceError: doSomething is not defined
9 Q! a1 _: s) O4 F7 f$ o7 Z! C在這裡直接調用了一個未定義的函數,導致出錯。在一些其他場景中,雖然程序運行正確無誤,但是仍然可以優化(這種優化並不是技巧上的),比如:
0 N8 n! W9 c" c5 m# D; Z* F6 Jfunction doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();
4 h7 X3 ^0 G" }) J+ O. O" _ 在這裡,doSomethingElse 函數之前由於有 return,因此這個函數調用將永遠不能執行,這種冗余代碼對整個程序來說毫無用處,可以去掉。6 E( O! p) Z) I) |! e7 U
對於 Closure Compiler 來說,它處理的對象是 js,不需要產生其他中間代碼或彙編代碼 / 機器碼,因此輸出的還是 js,但是是經過分析的、優化後的 js;另外,它也可以選擇輸出 parse tree(使用–print_tree 參數),所以,CC 的確完成了一個編譯器需要實現的功能。+ K9 N- M+ ?/ d: D8 S
CC 功能概述& B" ^1 }- V2 H7 z2 F
在詳細討論 CC 的高級模式前,還是簡明介紹一下功能體系。
0 C2 j+ a! D2 ]* B6 ?( I 編譯級別
5 i, J& Z" ^" x6 Q5 V4 @ CC 的 compilation_level 包括三個級別:
0 Y$ g+ I& k! E8 i( E, aWHITESPACE_ONLY
; ^: W9 W2 \9 R9 Q" H1 R- { \只刪除空白、註釋。2 U, Q3 ?6 c6 w `. x d
SIMPLE_OPTIMIZATIONS
! K* C/ ]# r2 i% L在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。/ ^( b/ x' i, a" l7 y* b/ L
ADVANCED_OPTIMIZATIONS5 T9 ?; R- ^" P' P
更加激進的重命名、移除垃圾代碼、內聯函數。
+ d" f* i5 b- w8 X! ~/ f 可以看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒做什麼真正的編譯工作,所以說,使用了高級模式的 CC 才是四肢健全的 CC 。
4 E8 Z9 u6 f$ w8 } 約束條件
. [2 ^& `) ^- V. n 使用 CC 有一定約束條件,這影響到我們的編碼風格:, y8 W& Y- T( v
WHITESPACE_ONLY% Y) I2 L4 G* W2 f
不認可 JS 1.5 以上版本的語言特性% n0 _) U% d5 {# g: _+ b
不保留註釋9 {0 o( {! \4 p7 A
SIMPLE_OPTIMIZATIONS
# `) E; p' Y4 W5 ]完全禁用 with 和 eval
- s0 F, f2 K* T3 f, `' p字符串中引用的函數名 / 參數名不會改動(CC 不改動所有字符串); W6 T. G* \& p+ S
ADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述) ^+ L- O; @8 d8 A
註解
: d$ ]9 Y: L6 @ Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。
3 R) @- k1 o$ }. K1 A% e 使用 CC 高級模式
" Y8 n3 R2 Q- j% f6 r/ O 在 CC 下,啟用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。7 ~& G' s6 P; t) T5 v
作為一個 compiler,CC 的高級模式下,額外的優化政策是:
, N* S# a; h6 n9 |3 i S: d9 l更激進的重命名,如 obj.property 改為 a.b,將深度過高的命名空間平坦化等;- k) |; Z$ `0 J0 x7 b
移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);' [1 U' u; H5 i5 G
將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。5 O0 @9 @) a4 H5 g& P
要達到高級模式的預期優化效果,開發者必須對自己做一些約束,因為 js 是弱類型、動態性的。否則
& i+ V7 [8 P! v: j: tjs 的這種靈活將使 compiler 無能為力。
+ S! F: S- L1 Y \% K 總體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。( g: W3 b. \" T) Z/ {
以下詳述具體的約束以及代碼的檢查 / 優化效果:
8 A: C+ c/ W5 \1 |$ T2 Z2 D 強類型的模擬$ d0 l6 }; N" M: U* C9 w9 }
@param 和 @type 中定義的類型會在編譯期間得到檢查,同樣避免了在運行時檢查,提高性能。
7 E& d1 Q" ]* ^& Z@const 標記常量,當常量被寫時會報錯。
7 ]( k" l! v. t, p模擬枚舉,將同類可枚舉常量定義為一個對像字面量,使用 @enum 標記:
! c) ?0 |& e- L6 y& f/ P9 X! avar STATUS = { LOADING: 3, COMPLETE: 4};
) D0 Q- B% W! \8 o$ h: p! H編譯結果中 STATUS.LOADING 會被直接替換為 3,其實完全模擬了 C 等語言中的枚舉。
- t6 Z9 Y6 H" k7 X. }2 M* \3 J使用 @constructor 標注函數為構造器,它僅能被實例化,而不可用作普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,否則報錯。這樣確保開發者不必在運行時判斷,構造器函數到底以怎樣的形式被調用。
j2 x! |) R8 E& }7 P在表達式中也可以使用 @type 來限定類型,這對於 JSON 特別有用,如
6 U, N; \; t. l. o. Rvar data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'});
% e7 A4 k- F8 ^在這裡 UserModel 是個構造器,也可以使用 @typedef 來自定義複雜的數據類型。: g& B& ~: A" l& C0 w2 @" Q
域可見性的模擬
) {: s# M4 O/ l- R6 P/ L$ H使用 @private 標注私有域,私有域被外部引用會報錯。開發者也可以按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒自己 / 協作者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者可以不必使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工作丟給CC,讓開發盡可能樸實簡單。' h4 v8 o5 W5 X: o/ J$ H, n
類似有 @protect
( I- i e$ I n 類系統的模擬% |# k4 e/ q' R( H+ ^0 T2 ]2 {
使用 @extends 標注繼承關係,繼承體系會被優化。 z2 \% Y* [' P I+ [
使用 @interface 標注接口,接口是類似 function ThisIsAInterface(obj) {}的函數體為空的構造器定義,編譯後將移除其相關代碼。同時,標注 @implements 的構造器必須實現implemented 的接口的所有方法(正如其他 OO 語言一樣),否則,CC 報錯。這同樣簡化了接口 / 實現的約束,靠 CC 來保證實現關係的可靠性。
- \9 R p3 F2 B 條件編譯的模擬# m. @4 A m, V7 E( @9 d) Q
使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態需要分離的模式。( Z; K. p |3 y2 q; `9 K
可以在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力…… s% L' E3 v0 b( q/ \
對像平坦化及屬性名縮減
2 U6 h9 t- Z! ?& O; \" N. z4 N對像屬性會被編譯為單變量,比如 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~之後 foo$bar 被進一步縮短。對像之所以能被平坦化是因為在 js 中對象可以看做是一群引用 / 原始數據類型的容器。
8 B0 s6 Q4 l$ m' ?但是,js 對像實際上更複雜,所以被平坦化後會帶來一些副作用,比如如果在對像(字面量)中使用 this 指針,則編譯後的結果會導致 this 指向錯誤。所以 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味著,在所謂類單例(對像字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。
, N T- N' ~, s2 i8 }在縮減對像屬性 / 方法的名稱長度時,有另外一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是因為 CC 始終不處理字符串中的內容,所以,var o = { longName: 0 }; o["longName"] 會被翻譯為var a = { b: 0 }; a["longName"] 導致出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。
) C) x6 _- y! N- {對於全局變量,如果出現以 window.property 的形式引用的,必須始終定義為 window.porperty 形式:
- Y, ^' K4 c& Vwindow.property = 1;var property = 1; // wrong!
l! e+ m5 o- a$ T6 T否則也會杯具,CC 可不會 window.property 翻譯為 window.a。
! k$ W5 G* q" U+ x9 G9 X+ f+ z$ B 垃圾代碼的移除- u% J- C- Z' I4 L) }, p
一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。
# Y( t/ U# c2 \% |- C9 _7 W8 ?/ ~在這種機制下,如果一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,因為這種動態特徵使得 CC 無法清楚方法是否確實在 for in 的時候被調用了。5 B h* b c9 i' J( i/ t
對於一些 unreachable 的代碼,CC 將報警告。/ g5 N, N: J9 U6 b# d3 W
如果要產出一份被調用的公共接口,例如庫,使用稱作 export 的方法將函數導出,防止函數定義被7 m& { Z1 T! i6 C0 G5 ?' n" O
CC 回收。具體的做法是將函數綁定到某個容器,比如:/ y0 H6 S; [# N* B. ]
function displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;
6 v5 e) m0 F' m3 u對於需要 export 的函數,均使用 quoted string 風格。9 R" `4 J9 h, |' `& ?
背後的思考! M- K8 z8 v. I& i4 b' _
根據以上高級模式優化的行為分析可知,CC 附加給開發者的約束主要有:0 L- S. Y" @4 H. c* I
強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯; ~2 F5 \# J5 e2 ^- l6 {
本身。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即通過開發者與 CC 達成一種, `7 R/ X1 ^9 _& d/ T7 D- h
編碼約定而規避掉。
, u8 S) n' X2 b嚴格要求區分面向開發者的代碼和面向機器的代碼。
* A' c5 w4 L: \$ m1 m% |雖然不像 C 等語言會編譯產生目標代碼,但是 CC 在一定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗余代碼去除。這和第一點其實是一脈相承的,同樣要求開發者將關注點轉移到開發本身。! N& I/ M m9 i' h4 m7 T' T; s. Y
使用規範化的接口方式。
% }$ }* ?8 S. h) a# w; R這不僅包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用同樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,但是帶來的好處是明顯的:可讀、可控、一致性。
; @* f2 O) q' k$ P9 W* W, [) s 曾經有讀過 Closure Library 源碼的童鞋評論道:: D N) G; e# ]1 X
Google 根本不懂怎麼寫 javascript!代碼裡面各種冗余,並且充滿了 java 的味道!* ^) `# ?# @( C, D9 N5 t! G
當時確實也有這種感覺,比如 Google 把 if(foo) 寫作 if(foo != undefined) 等等。 a7 i2 L% P$ j5 q0 U! u s
Javascript 固然充滿了豐富的動態特徵,而且很多特性非常優雅,能夠讓代碼簡潔精悍,或者構造出一些4 ~% B* v8 u4 l/ k+ f }: \
令人驚歎的技巧,但是也會產生一些副作用:/ \: w9 e; _ `* e3 I
首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西需要經過一番運算才能得出結論。
) K3 [6 I8 O% a比如 js 中的極晚綁定,再比如標識符運行時重寫。
% y J5 V! F& ~7 F+ C& j. t其次的問題是執行性能。一個比較經典的眾 js 工程師都在使用的技巧就是「模擬函數重載」——
! w( f5 O) p8 l- \在函數體內判斷 arguments 的特徵,從而對應給出不同的邏輯。由於缺乏強類型,js 本身不能具備
# @8 C% d& U { Q真正的重載,但是運行時的判斷在帶來靈活性的同時,必然會多出很多模擬重載的邏輯,降低性能。3 v$ N! W8 S6 u2 @* j
在今年的 D2 大會上,Hedger 童鞋指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵作戰還可以,但是一旦碰到 army(軍隊,比如 Google 團隊這樣的 )就是個悲劇。4 M, j" ` W# d/ b. K4 D
我比較欣賞這個比喻,大團隊要良好地協作,必需遵循一定的規範和限制,優先保證可讀性和一致性,與此同時* P% x& l8 }% d+ [. p! ^: d
失去的是奇技淫巧、自由靈活。所以採用何種編程風格、理念,需要具體問題具體分析…………
( U: f" V! N A2 @& h5 ]( p# J: H 至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑒的。
3 z# a: m- O* h 最後附上 CC 的常用命令選項……選項實在是有夠多……6 r7 H! [3 r+ K5 R9 ?% ^
CC 常用命令選項
2 M; T0 b9 W* ^" {6 N–charset VAL 對所有文件定義的編碼格式
% u; y% e1 c1 A–compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]! I0 R2 z/ H- d% ~+ d
設定編譯級別
/ Z2 _, R5 J; M/ v' j, H6 k–debug 開啟 debug 選項5 u$ x; {8 q) j ~3 R
–define (–D, -D) VAL 設定文件中使用 @define 標注的開關值,即條件編譯& w" Z& O. m }
–externs VAL 編譯代碼需要調用未編譯的代碼時,使用它6 z1 K3 I, f5 Y1 J( T. z
–formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出
& J, _" i. g( n# b–js VAL 輸入文件,多指定多個,將會被合併; v; Z2 k* F& a1 r1 i. w$ g( ]
–jsoutputfile VAL 輸出文件,如果不指定的話,直接輸出到 standard output 流- t! s5 ?/ p4 v$ G, [, x
–module VAL 定義模塊, i& l2 r5 ^2 g* L
–output_manifest VAL 打印編譯文件清單% X$ h) `' e6 Q+ i( D
–print_tree 打印語法分析樹
: Q7 y9 z" e# P2 ~–warning_level [QUIET | DEFAULT | VERBOSE] 設定報錯模式 |