本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考' |% ]+ W. a5 ~/ L0 i
CC 是真正的編譯器7 B- M' J+ _ I. T5 {
Closure Compiler 和 YUICompressor 並不是同類產品,雖然 CC 和 YC 同樣產出壓縮後的 JS 文件,但是 YC 只做了詞法上的掃瞄,而 CC 並不只是一個 compressor 那麼簡單,器如其名,它是一個compiler。- l1 P& X) {4 c
對於一個 compiler,一般地,它需要做到:
/ _$ N+ g; Y% r8 `, @1 l8 F檢查源文本中語法、語義、語用上的錯誤;
- m+ h* ~1 y% U0 {根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;
9 T5 ?% Q, ^; ]6 n" D1 ~( ]3 O優化。
/ d; s: i# i$ R* g+ o 代碼錯誤一般來自三個方面:
, c" T2 d& W7 j7 l0 U6 _7 p語法(Syntax)
; E) Q% S' x o1 I表示構成語言句子的各個記號之間的組合規律。大體上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……1 `: ~8 u0 E- b F$ N
語法上的錯誤,如:
2 Z6 D4 d- T3 O+ i0 CdoSomething(;) // SyntaxError: Unexpected token ;8 D- ^; E, J7 i2 I; ?0 M
根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並沒有配對),因此報錯。
& r* x( U0 ?$ O語義(Semantics)
8 X5 ~5 |+ O4 U' |: B表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 需要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。
7 G# ?8 Y) ?2 ]* S0 x, g* o語義上的錯誤,如:, \/ ]( J' l& [+ \" @
0 = {}; // ReferenceError: Invalid left-hand side in assignment
5 S1 i5 J8 S2 o8 n$ H/ {根據賦值運算符 = 的意義,左操作數不能為字面量,所以雖然這個賦值語句包含了必需的左操作數、運算符、右操作數,仍然出錯。7 C* A0 q/ v$ Z9 O* j* S! }
語用(Pragmatics)5 G4 ?( h' k6 K+ I9 i1 h
表示在各個記號所出現的行為中,它們的來源、使用和影響。
, W4 A. l6 Y5 `8 w# y3 d語用上的錯誤,如:
) p; M# m# w8 v$ c( _& z2 W# [doSomething(); // ReferenceError: doSomething is not defined
$ Q/ O% p7 k$ ?) `& A2 E在這裡直接調用了一個未定義的函數,導致出錯。在一些其他場景中,雖然程序運行正確無誤,但是仍然可以優化(這種優化並不是技巧上的),比如:
0 r& ^7 L8 N" j% k5 ?3 {function doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();
; F/ T) E, Z% i h+ `" I, Z8 N 在這裡,doSomethingElse 函數之前由於有 return,因此這個函數調用將永遠不能執行,這種冗余代碼對整個程序來說毫無用處,可以去掉。
1 k& N+ v, L9 ]- X9 J2 z 對於 Closure Compiler 來說,它處理的對象是 js,不需要產生其他中間代碼或彙編代碼 / 機器碼,因此輸出的還是 js,但是是經過分析的、優化後的 js;另外,它也可以選擇輸出 parse tree(使用–print_tree 參數),所以,CC 的確完成了一個編譯器需要實現的功能。: Y6 \) g6 a7 W- v. A4 \* |
CC 功能概述) ~7 H" K9 m/ ]- i8 N+ Q
在詳細討論 CC 的高級模式前,還是簡明介紹一下功能體系。
7 C9 Q( T8 B! p* T5 F 編譯級別! r2 d8 A; H4 S1 A5 J
CC 的 compilation_level 包括三個級別:
) C; ~: t$ _7 v/ B, m% o$ XWHITESPACE_ONLY* o+ y; v! k8 K9 l3 F5 o
只刪除空白、註釋。5 _: E5 m+ q9 [% L6 k+ Y5 Y
SIMPLE_OPTIMIZATIONS9 b/ D$ F3 y1 w* j- D% @; R- e! J
在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。# R' R8 P2 E; Q
ADVANCED_OPTIMIZATIONS
4 d& q" c1 N7 Y4 V7 o更加激進的重命名、移除垃圾代碼、內聯函數。
1 v0 ^4 G; h! j4 U4 t 可以看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒做什麼真正的編譯工作,所以說,使用了高級模式的 CC 才是四肢健全的 CC 。# S" B: p O# W1 ^3 W* J' j
約束條件
% G! e3 x j b# V 使用 CC 有一定約束條件,這影響到我們的編碼風格:% R# S3 A/ ?3 a' a
WHITESPACE_ONLY
; J' i' [% y: ?& ?不認可 JS 1.5 以上版本的語言特性# B0 m$ P3 Z2 F$ U- F T
不保留註釋
: n' X# W2 J$ |. x- XSIMPLE_OPTIMIZATIONS* [ r) f! R* _$ v+ d5 m) H
完全禁用 with 和 eval
9 f. J* ^! A- _% z# C% \% T字符串中引用的函數名 / 參數名不會改動(CC 不改動所有字符串)
1 G$ ]7 ~$ O- g" AADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述
& G2 I8 M: O! E8 l: E* N3 v 註解% n* x8 H" E. D3 @+ f' `
Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。
, ~+ p! T0 o( N/ v/ W 使用 CC 高級模式
) N( R$ G' S7 o4 \1 c 在 CC 下,啟用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。 X7 G, |5 A/ {5 J6 B, a" i
作為一個 compiler,CC 的高級模式下,額外的優化政策是:
! P( G# [1 j1 ~- J5 w& u9 b. C1 d2 L更激進的重命名,如 obj.property 改為 a.b,將深度過高的命名空間平坦化等;
) u @. H) h# Y+ h- s4 h1 }移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);
" p6 B$ Y X" [將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。5 a) V* L2 Y, w) k1 v
要達到高級模式的預期優化效果,開發者必須對自己做一些約束,因為 js 是弱類型、動態性的。否則
, u- }- y& N; A3 L% H2 O; njs 的這種靈活將使 compiler 無能為力。
: n" q. \# U5 L5 \- ^3 q: J) b 總體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。+ P9 G) c% M. ~
以下詳述具體的約束以及代碼的檢查 / 優化效果:
7 i& p) E* H+ Y, L* C! y" o 強類型的模擬
: j3 u4 S: t2 c2 E+ B- T@param 和 @type 中定義的類型會在編譯期間得到檢查,同樣避免了在運行時檢查,提高性能。
& o3 P/ `. p' U. y4 l) _@const 標記常量,當常量被寫時會報錯。
! l$ v+ R8 M" l7 ~) k模擬枚舉,將同類可枚舉常量定義為一個對像字面量,使用 @enum 標記:: n+ C* }2 C& h
var STATUS = { LOADING: 3, COMPLETE: 4};3 L) m3 r' x" b j R6 z$ X/ Y
編譯結果中 STATUS.LOADING 會被直接替換為 3,其實完全模擬了 C 等語言中的枚舉。6 y" [. H0 `+ l' e$ I
使用 @constructor 標注函數為構造器,它僅能被實例化,而不可用作普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,否則報錯。這樣確保開發者不必在運行時判斷,構造器函數到底以怎樣的形式被調用。. H+ L: u* |, Z( V
在表達式中也可以使用 @type 來限定類型,這對於 JSON 特別有用,如! H. P f4 B- U( O. P
var data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'}); I2 V8 R3 {' X- z P
在這裡 UserModel 是個構造器,也可以使用 @typedef 來自定義複雜的數據類型。
6 w! d4 J; }! H# G) ? 域可見性的模擬
+ S5 Y- w: d+ h4 m, f使用 @private 標注私有域,私有域被外部引用會報錯。開發者也可以按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒自己 / 協作者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者可以不必使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工作丟給CC,讓開發盡可能樸實簡單。
3 l M) i7 G' ], A& x! F0 k- R' y6 n類似有 @protect( O, r5 D6 c0 P1 l1 a
類系統的模擬! P* V, c. ^6 ^2 }9 x
使用 @extends 標注繼承關係,繼承體系會被優化。
9 V, y7 g9 a1 W, T使用 @interface 標注接口,接口是類似 function ThisIsAInterface(obj) {}的函數體為空的構造器定義,編譯後將移除其相關代碼。同時,標注 @implements 的構造器必須實現implemented 的接口的所有方法(正如其他 OO 語言一樣),否則,CC 報錯。這同樣簡化了接口 / 實現的約束,靠 CC 來保證實現關係的可靠性。1 \! c: m% P$ N) [- W8 K( C
條件編譯的模擬
8 z4 i) v, ^8 z+ `5 d6 j) m w# ]使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態需要分離的模式。+ u3 Z6 M2 o1 E% V
可以在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力……/ _- Y. g% q- K& P1 n: m
對像平坦化及屬性名縮減& _! C' o$ X! g( r: ?! o
對像屬性會被編譯為單變量,比如 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~之後 foo$bar 被進一步縮短。對像之所以能被平坦化是因為在 js 中對象可以看做是一群引用 / 原始數據類型的容器。
: M: L& i W+ Y, _ v- b但是,js 對像實際上更複雜,所以被平坦化後會帶來一些副作用,比如如果在對像(字面量)中使用 this 指針,則編譯後的結果會導致 this 指向錯誤。所以 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味著,在所謂類單例(對像字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。6 h( X' b; g$ H. N
在縮減對像屬性 / 方法的名稱長度時,有另外一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是因為 CC 始終不處理字符串中的內容,所以,var o = { longName: 0 }; o["longName"] 會被翻譯為var a = { b: 0 }; a["longName"] 導致出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。% s& o( d* o# |# S
對於全局變量,如果出現以 window.property 的形式引用的,必須始終定義為 window.porperty 形式:, R9 X+ s; m: {8 d: m0 Y' N0 Y
window.property = 1;var property = 1; // wrong!8 l: m! j' E' Y7 {! q
否則也會杯具,CC 可不會 window.property 翻譯為 window.a。' l7 _% j6 @- _
垃圾代碼的移除
$ H) \0 y$ _4 }5 n( B! m一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。
; e3 J1 s, g- H. ?3 h( r2 T在這種機制下,如果一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,因為這種動態特徵使得 CC 無法清楚方法是否確實在 for in 的時候被調用了。2 _7 C X, S0 T
對於一些 unreachable 的代碼,CC 將報警告。- B5 J: u# ?- D" a1 U
如果要產出一份被調用的公共接口,例如庫,使用稱作 export 的方法將函數導出,防止函數定義被, ~/ s0 M1 E, h, J% v
CC 回收。具體的做法是將函數綁定到某個容器,比如:
. `% u+ Q4 N! |3 s+ n$ T* bfunction displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;
6 d' l+ u6 U% D對於需要 export 的函數,均使用 quoted string 風格。2 O' i1 s7 ]5 f8 e* g0 y9 G) G0 c
背後的思考0 e9 T9 Z( M- ^
根據以上高級模式優化的行為分析可知,CC 附加給開發者的約束主要有:8 W) C+ _( X- U) q' t1 O
強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯
4 m. v+ E$ q j7 n6 \9 h本身。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即通過開發者與 CC 達成一種7 P4 Q- h+ o3 }( ?
編碼約定而規避掉。4 @. G( M) \' N6 T: q
嚴格要求區分面向開發者的代碼和面向機器的代碼。/ B+ `2 t" i7 e2 W
雖然不像 C 等語言會編譯產生目標代碼,但是 CC 在一定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗余代碼去除。這和第一點其實是一脈相承的,同樣要求開發者將關注點轉移到開發本身。
- O" `& D* z) z使用規範化的接口方式。; d) i# Q% `: V
這不僅包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用同樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,但是帶來的好處是明顯的:可讀、可控、一致性。
% u% M) c, a) A# m( T 曾經有讀過 Closure Library 源碼的童鞋評論道:
) u$ K4 R. P1 S. G0 NGoogle 根本不懂怎麼寫 javascript!代碼裡面各種冗余,並且充滿了 java 的味道!/ o+ B! p. \1 F. k' Y
當時確實也有這種感覺,比如 Google 把 if(foo) 寫作 if(foo != undefined) 等等。
- u* V/ P0 g- n0 u) m Javascript 固然充滿了豐富的動態特徵,而且很多特性非常優雅,能夠讓代碼簡潔精悍,或者構造出一些; W$ p' U+ h+ o" G! s& R' }9 I
令人驚歎的技巧,但是也會產生一些副作用:2 L3 K0 Y$ B d7 v& s6 L; t: W/ u4 Q
首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西需要經過一番運算才能得出結論。7 Z! D6 P/ i5 J( C; x! Y/ g
比如 js 中的極晚綁定,再比如標識符運行時重寫。- u9 H4 p8 O v( u3 k8 P& f4 |) U+ y
其次的問題是執行性能。一個比較經典的眾 js 工程師都在使用的技巧就是「模擬函數重載」——2 Z4 b j; @5 w+ u
在函數體內判斷 arguments 的特徵,從而對應給出不同的邏輯。由於缺乏強類型,js 本身不能具備
4 T* f& a: O& j$ d# t真正的重載,但是運行時的判斷在帶來靈活性的同時,必然會多出很多模擬重載的邏輯,降低性能。$ j# P7 `& C1 D7 d
在今年的 D2 大會上,Hedger 童鞋指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵作戰還可以,但是一旦碰到 army(軍隊,比如 Google 團隊這樣的 )就是個悲劇。
+ T0 x p5 i" Z8 X6 G+ o% ~ 我比較欣賞這個比喻,大團隊要良好地協作,必需遵循一定的規範和限制,優先保證可讀性和一致性,與此同時
; h; P9 D. L6 ^& O9 `- c% }失去的是奇技淫巧、自由靈活。所以採用何種編程風格、理念,需要具體問題具體分析…………2 ]+ l) k9 Y: `9 u
至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑒的。
5 B( o& s* N: c# R 最後附上 CC 的常用命令選項……選項實在是有夠多……
, t5 s% q2 O/ F, L& F/ k7 k CC 常用命令選項* T; r3 F0 b m' D' G" m
–charset VAL 對所有文件定義的編碼格式" x3 l1 Y* A/ Z( ~+ W2 e- G2 |
–compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]
* r$ n* F5 p5 s: x1 z設定編譯級別
0 q2 J4 x. ? I% _( `1 x0 C, |–debug 開啟 debug 選項
4 Q; Q8 L7 S# s' V–define (–D, -D) VAL 設定文件中使用 @define 標注的開關值,即條件編譯
/ L* }0 U% {6 y3 ~4 ]–externs VAL 編譯代碼需要調用未編譯的代碼時,使用它
/ L8 j; `; _- y# h6 N–formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出
7 U# F. i: c2 j–js VAL 輸入文件,多指定多個,將會被合併; d! p3 y, ~* F3 G4 b2 K
–jsoutputfile VAL 輸出文件,如果不指定的話,直接輸出到 standard output 流
9 n D/ {$ N9 R' O, k* s–module VAL 定義模塊" v: K4 D S/ _/ @
–output_manifest VAL 打印編譯文件清單" \4 o. G' A7 N' ~
–print_tree 打印語法分析樹
( @: A- y% u9 l R4 B6 K–warning_level [QUIET | DEFAULT | VERBOSE] 設定報錯模式 |