过期域名预定抢注

 找回密碼
 免费注册

Google Closure Compiler 高級模式及更多思考

[複製鏈接]
發表於 2011-7-10 09:23:36 | 顯示全部樓層 |閱讀模式
 本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考" T# j" F& t: {8 i3 }; f  F9 B
  CC 是真正的編譯器; K/ I0 D' }3 n, m5 |3 c
  Closure Compiler 和 YUICompressor 並不是同類產品,雖然 CC 和 YC 同樣產出壓縮後的 JS 文件,但是 YC 只做了詞法上的掃瞄,而 CC 並不只是一個 compressor 那麼簡單,器如其名,它是一個compiler。
2 U: M2 t" M. ^" ~4 r% ~( F  對於一個 compiler,一般地,它需要做到:" k0 ]' P9 ~9 C3 Z3 m
檢查源文本中語法、語義、語用上的錯誤;
; i  r# d* ~1 @# F4 E9 ], l* `/ Z根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;% q8 C* r) j+ E- D
優化。
+ d6 D- I0 ~; P$ m2 g; k, D  代碼錯誤一般來自三個方面:
  n9 s2 ^* ~7 l語法(Syntax)
: t' o) |  W4 U' a  `  `' \8 I表示構成語言句子的各個記號之間的組合規律。大體上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……
3 l8 J5 l+ t. e" X& \! [語法上的錯誤,如:
! t' y7 C; c7 x/ T% u9 }doSomething(;) // SyntaxError: Unexpected token ;
# e% J1 t( V. F9 i+ I根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並沒有配對),因此報錯。: W- M$ F# ?- t7 N
語義(Semantics)
  H5 G; u/ k3 L* }6 S& H表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 需要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。$ H4 s+ h3 [; i6 i4 g
語義上的錯誤,如:
" N9 W3 {) H. R! c* H1 z4 P0 = {}; // ReferenceError: Invalid left-hand side in assignment
8 A8 O  A' Y: I/ p4 b  K! e  I% I# b根據賦值運算符 = 的意義,左操作數不能為字面量,所以雖然這個賦值語句包含了必需的左操作數、運算符、右操作數,仍然出錯。
( ~; H2 p5 h+ s4 }2 E# |  z語用(Pragmatics)' ?8 C' D+ N4 {8 e# n+ ^6 n
表示在各個記號所出現的行為中,它們的來源、使用和影響。) l. B3 Q# _* C$ P, J6 n
語用上的錯誤,如:9 @3 M/ K( k. h% z/ g3 \7 ^3 @
doSomething(); // ReferenceError: doSomething is not defined. S$ w3 v# Y+ H: J5 \& _5 ^2 }
在這裡直接調用了一個未定義的函數,導致出錯。在一些其他場景中,雖然程序運行正確無誤,但是仍然可以優化(這種優化並不是技巧上的),比如:: Y4 l% X: t( k. w9 p' s
function doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();7 }1 P( U5 Q% u8 G6 ~
  在這裡,doSomethingElse 函數之前由於有 return,因此這個函數調用將永遠不能執行,這種冗余代碼對整個程序來說毫無用處,可以去掉。
" z6 _) C- a8 E3 g  對於 Closure Compiler 來說,它處理的對象是 js,不需要產生其他中間代碼或彙編代碼 / 機器碼,因此輸出的還是 js,但是是經過分析的、優化後的 js;另外,它也可以選擇輸出 parse tree(使用–print_tree 參數),所以,CC 的確完成了一個編譯器需要實現的功能。
$ _: I! @+ F$ M# J- J  CC 功能概述
9 T! t' Z3 \( y7 a  在詳細討論 CC 的高級模式前,還是簡明介紹一下功能體系。
  }( M1 }8 Q! r' ?! M: x- K5 K, Z  編譯級別6 x: o: }+ M* V5 l5 _
  CC 的 compilation_level 包括三個級別:* f4 g1 g8 Q+ A6 u: b
WHITESPACE_ONLY/ k0 b( M9 \: S
只刪除空白、註釋。
! B- x8 b3 a5 s' f) n' \SIMPLE_OPTIMIZATIONS
& h7 t7 P. {2 B: {# r0 M在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。
& K9 o" ~, }# c8 Q: NADVANCED_OPTIMIZATIONS: I% s8 e& Y& ~( d
更加激進的重命名、移除垃圾代碼、內聯函數。% T) H2 b, k7 t5 r9 g$ C3 f, [: h# L) G
  可以看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒做什麼真正的編譯工作,所以說,使用了高級模式的 CC 才是四肢健全的 CC 。( R1 u3 S& m6 Y+ N% D" d5 D% h
  約束條件3 a/ ?* L3 x( x( A* w
  使用 CC 有一定約束條件,這影響到我們的編碼風格:- z! A4 u  M* f/ Z7 a
WHITESPACE_ONLY8 Q2 v$ O* O2 J) ^
不認可 JS 1.5 以上版本的語言特性5 `, R! O8 J' B' l6 m3 K
不保留註釋
% v$ l7 B, Y! C5 o- X7 PSIMPLE_OPTIMIZATIONS- D, ^8 Q2 w$ q' {8 X
完全禁用 with 和 eval
4 T. A6 o8 Z% L, ~; V+ F3 g0 V字符串中引用的函數名 / 參數名不會改動(CC 不改動所有字符串)
2 Y6 M5 D1 T- t5 y4 k" E% z0 i* _ADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述
5 y5 S& ?8 D0 A# W  註解, }. ]8 h3 N4 ~% z! Q( ~
  Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。
9 j+ d% U+ {0 w! U! ^# U) S. Z2 }6 Y  使用 CC 高級模式
) |! J4 i3 i; I0 ^% ~$ G  在 CC 下,啟用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。; o" q- Z# K5 }& W4 j
  作為一個 compiler,CC 的高級模式下,額外的優化政策是:
- z$ w* h. l. _5 L. {$ ]8 s/ z更激進的重命名,如 obj.property 改為 a.b,將深度過高的命名空間平坦化等;
' x- A- s; p' w/ {移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);+ `0 q: l9 S* G3 f0 `0 e4 M$ v
將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。
& H: m" l* w# G8 ^4 t+ l  要達到高級模式的預期優化效果,開發者必須對自己做一些約束,因為 js 是弱類型、動態性的。否則2 t4 R7 g, T7 K7 o8 k
js 的這種靈活將使 compiler 無能為力。
3 A% u7 G( t) c+ ^. M6 r  總體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。
# E6 W* E3 t) s  以下詳述具體的約束以及代碼的檢查 / 優化效果:
/ i2 N' n9 \. a$ I+ T2 a  強類型的模擬5 ]( C4 Y# u' B4 E3 w
@param 和 @type 中定義的類型會在編譯期間得到檢查,同樣避免了在運行時檢查,提高性能。
% C; x  M. b7 L@const 標記常量,當常量被寫時會報錯。
: {) F; v: ^8 l& Z4 P1 {模擬枚舉,將同類可枚舉常量定義為一個對像字面量,使用 @enum 標記:; m9 c1 o3 U4 z2 W5 R& G0 a4 n4 _
var STATUS = { LOADING: 3, COMPLETE: 4};3 q( R5 H+ b- ~, n9 ~' a
編譯結果中 STATUS.LOADING 會被直接替換為 3,其實完全模擬了 C 等語言中的枚舉。
6 R& }& N/ |8 E# ]5 X1 C$ d/ q使用 @constructor 標注函數為構造器,它僅能被實例化,而不可用作普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,否則報錯。這樣確保開發者不必在運行時判斷,構造器函數到底以怎樣的形式被調用。% ?/ w$ \: R" I
在表達式中也可以使用 @type 來限定類型,這對於 JSON 特別有用,如. U/ ]+ z) }; H- J5 ^
var data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'});
, \( ?6 L0 t, `5 {( l  U在這裡 UserModel 是個構造器,也可以使用 @typedef 來自定義複雜的數據類型。
$ }9 ?) B0 ~  r" ~0 u, A  域可見性的模擬
9 q5 V4 B. f* |# B- g3 E8 ?0 s& W使用 @private 標注私有域,私有域被外部引用會報錯。開發者也可以按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒自己 / 協作者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者可以不必使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工作丟給CC,讓開發盡可能樸實簡單。
- ]& s/ R6 E- E. D0 r; r9 w類似有 @protect0 Z  c9 w! @( D
  類系統的模擬7 n* [+ K2 H. |. T% o
使用 @extends 標注繼承關係,繼承體系會被優化。. a0 Y; }: t" a  o9 r
使用 @interface 標注接口,接口是類似 function ThisIsAInterface(obj) {}的函數體為空的構造器定義,編譯後將移除其相關代碼。同時,標注 @implements 的構造器必須實現implemented 的接口的所有方法(正如其他 OO 語言一樣),否則,CC 報錯。這同樣簡化了接口 / 實現的約束,靠 CC 來保證實現關係的可靠性。: b. I9 W3 U# o; b$ @% h
  條件編譯的模擬! @9 c. F: @0 y
使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態需要分離的模式。7 ~9 ^" {, z8 {4 p) S: l
可以在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力……
% {$ Q" N( N  k  K6 ~) E& ]' Z) ?. N  對像平坦化及屬性名縮減. y* v" c, m- h. L  g
對像屬性會被編譯為單變量,比如 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~之後 foo$bar 被進一步縮短。對像之所以能被平坦化是因為在 js 中對象可以看做是一群引用 / 原始數據類型的容器。
: D4 c9 x6 t9 T3 {' g# {但是,js 對像實際上更複雜,所以被平坦化後會帶來一些副作用,比如如果在對像(字面量)中使用 this 指針,則編譯後的結果會導致 this 指向錯誤。所以 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味著,在所謂類單例(對像字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。
  i' E8 v: z* W4 [; u" e' u* V9 K1 F; H在縮減對像屬性 / 方法的名稱長度時,有另外一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是因為 CC 始終不處理字符串中的內容,所以,var o = { longName: 0 }; o["longName"] 會被翻譯為var a = { b: 0 }; a["longName"] 導致出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。
8 q. V# g; Y$ H& t. ~: w% l6 Q0 L對於全局變量,如果出現以 window.property 的形式引用的,必須始終定義為 window.porperty 形式:
$ F* D- A) @; Mwindow.property = 1;var property = 1; // wrong!
, {2 I1 |. K& h- |! I否則也會杯具,CC 可不會 window.property 翻譯為 window.a。
8 F- \4 ~) A% G. V  垃圾代碼的移除. D0 f0 a- @; w* l1 H
一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。
/ m- R, n2 ^+ N, t- s- d9 Z  ]在這種機制下,如果一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,因為這種動態特徵使得 CC 無法清楚方法是否確實在 for in 的時候被調用了。
. F, g5 g" g: @( ]3 s對於一些 unreachable 的代碼,CC 將報警告。* h% s- V+ m' d% B6 i5 L
如果要產出一份被調用的公共接口,例如庫,使用稱作 export 的方法將函數導出,防止函數定義被  R# w7 V: I, ^8 x  y" u) H
CC 回收。具體的做法是將函數綁定到某個容器,比如:4 L. y9 g# E- L. \- D2 F" X
function displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;
) s+ A; z5 c6 w" a6 w! n7 {$ X- D對於需要 export 的函數,均使用 quoted string 風格。0 z2 q: `/ C! l) q, o! y' y
  背後的思考
+ N$ i9 z, z# W9 D! ^+ R4 Z  根據以上高級模式優化的行為分析可知,CC 附加給開發者的約束主要有:
" F9 [6 K0 O: v, x* J9 i強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯5 K6 d  W. {; g0 K, U; [
本身。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即通過開發者與 CC 達成一種
+ l- L# f: T+ h$ X# o9 k編碼約定而規避掉。6 u8 |; [& J& `" B" T4 b# e  X
嚴格要求區分面向開發者的代碼和面向機器的代碼。5 b; v# k' o) `
雖然不像 C 等語言會編譯產生目標代碼,但是 CC 在一定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗余代碼去除。這和第一點其實是一脈相承的,同樣要求開發者將關注點轉移到開發本身。
' f" b& ^- @4 [" C8 i# k+ @使用規範化的接口方式。
- N+ d4 z) F7 ?2 Z+ z* \. N這不僅包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用同樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,但是帶來的好處是明顯的:可讀、可控、一致性。
7 Y6 `6 y' b, G1 W! k  曾經有讀過 Closure Library 源碼的童鞋評論道:
. u; d  |$ s3 q1 ]Google 根本不懂怎麼寫 javascript!代碼裡面各種冗余,並且充滿了 java 的味道!
7 t" z; ~' m' O! `, O8 G  當時確實也有這種感覺,比如 Google 把 if(foo) 寫作 if(foo != undefined) 等等。: ]$ j$ [, M- |6 s* U! |  D$ i( [
  Javascript 固然充滿了豐富的動態特徵,而且很多特性非常優雅,能夠讓代碼簡潔精悍,或者構造出一些
% S: ^1 @* x& ?% w令人驚歎的技巧,但是也會產生一些副作用:! \- E) ?. R0 m
首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西需要經過一番運算才能得出結論。' U" h; G4 k" _& g" V! @! @
比如 js 中的極晚綁定,再比如標識符運行時重寫。
6 J, w+ m& f  P+ ~$ F9 {其次的問題是執行性能。一個比較經典的眾 js 工程師都在使用的技巧就是「模擬函數重載」——
/ b' q/ A5 z: M9 ~4 K1 P/ I9 X/ w在函數體內判斷 arguments 的特徵,從而對應給出不同的邏輯。由於缺乏強類型,js 本身不能具備
; a4 [* o* t& ^) @8 I- p真正的重載,但是運行時的判斷在帶來靈活性的同時,必然會多出很多模擬重載的邏輯,降低性能。
3 G& g1 W5 {4 B; S' F6 K  在今年的 D2 大會上,Hedger 童鞋指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵作戰還可以,但是一旦碰到 army(軍隊,比如 Google 團隊這樣的 )就是個悲劇。
" R1 s. ^) P+ v' N  我比較欣賞這個比喻,大團隊要良好地協作,必需遵循一定的規範和限制,優先保證可讀性和一致性,與此同時
! q0 m  U' ^* K7 m. q# k失去的是奇技淫巧、自由靈活。所以採用何種編程風格、理念,需要具體問題具體分析…………1 o' p" P% I; P' _/ f" A. `' ?
  至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑒的。
. ?* O. b+ L( `2 _, F; J% C  最後附上 CC 的常用命令選項……選項實在是有夠多……
- d% i3 S! b# @, e0 }6 I5 P  CC 常用命令選項
6 K9 z& V9 `+ [- Y& |3 ~  T* F–charset VAL 對所有文件定義的編碼格式
' g( w! I7 C3 S7 c3 I, F–compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]4 g% l8 ^8 i7 c& B: x; e1 a, ?
設定編譯級別% k1 ~3 ?3 O' {8 H
–debug 開啟 debug 選項
2 N; k. ~4 }' D+ V) Z# v8 E–define (–D, -D) VAL 設定文件中使用 @define 標注的開關值,即條件編譯
1 c+ V6 y2 Z& F' O8 k5 p–externs VAL 編譯代碼需要調用未編譯的代碼時,使用它
( N0 w. T$ L( E–formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出
4 j0 Q5 B' L5 t% j–js VAL 輸入文件,多指定多個,將會被合併
) W& V4 b8 P5 e0 G4 N–jsoutputfile VAL 輸出文件,如果不指定的話,直接輸出到 standard output 流
. m/ n, y, `4 H) \- S4 r1 j7 C–module VAL 定義模塊
  r. z! d, D) A2 c$ g–output_manifest VAL 打印編譯文件清單" X3 m* F! \% j  j# S
–print_tree 打印語法分析樹
6 W: q( v8 C  X* P) g–warning_level [QUIET | DEFAULT | VERBOSE] 設定報錯模式
發表於 2011-7-10 14:17:30 | 顯示全部樓層
這個是做什麼用的
回復 给力 爆菊

使用道具 舉報

發表於 2011-7-11 23:23:30 | 顯示全部樓層
這個基本上 不懂!
回復 给力 爆菊

使用道具 舉報

發表於 2011-7-12 14:36:33 | 顯示全部樓層
貌似這個東西太深奧......
回復 给力 爆菊

使用道具 舉報

您需要登錄後才可以回帖 登錄 | 免费注册

本版積分規則

點基

GMT+8, 2026-8-15 11:22

By DZ X3.5

小黑屋

快速回復 返回頂部 返回列表