过期域名预定抢注

 找回密碼
 免费注册

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

[複製鏈接]
發表於 2011-7-10 09:23:36 | 顯示全部樓層 |閱讀模式
 本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考" N# ]3 a4 n* O  ~3 f0 ^5 X3 T
  CC 是真正的編譯器6 n1 o% j7 Z; D2 ?* `+ p* j" M
  Closure Compiler 和 YUICompressor 並不是同類產品,雖然 CC 和 YC 同樣產出壓縮後的 JS 文件,但是 YC 只做了詞法上的掃瞄,而 CC 並不只是一個 compressor 那麼簡單,器如其名,它是一個compiler。: p( k8 H. V) _+ [1 m1 \1 M
  對於一個 compiler,一般地,它需要做到:1 y" I* x$ D/ }: t" ?" F
檢查源文本中語法、語義、語用上的錯誤;
) e6 w4 f; m, Y0 c- R; H3 ~3 u根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;. y- s0 J& c0 ~, w! Q* o/ _
優化。7 u; K- B/ S9 a6 M" J. J
  代碼錯誤一般來自三個方面:
/ V9 u4 P2 J1 r3 G5 a3 {% L; J語法(Syntax)8 w- B3 f+ W3 h6 G- y& c
表示構成語言句子的各個記號之間的組合規律。大體上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……* @9 U+ ~4 b  p4 f( I5 ], S
語法上的錯誤,如:; v8 T& x( H/ S: w9 k6 v
doSomething(;) // SyntaxError: Unexpected token ;: t/ M- m" S3 H5 v/ z
根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並沒有配對),因此報錯。) b/ V- ^7 X5 {0 v9 r
語義(Semantics)$ ?# K0 i) L9 F+ j: `
表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 需要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。
. n* @& _3 k" ]1 t+ U5 U! x* r  t語義上的錯誤,如:3 Y3 S9 {6 N8 i/ _) f6 T# y( M% _
0 = {}; // ReferenceError: Invalid left-hand side in assignment, H9 r) f8 v. B0 k- Y. ~% [
根據賦值運算符 = 的意義,左操作數不能為字面量,所以雖然這個賦值語句包含了必需的左操作數、運算符、右操作數,仍然出錯。
0 H; o* j1 T: y8 o- m8 C* @# L( a! M. S語用(Pragmatics)
8 Q0 \; |: o# Z$ a: |表示在各個記號所出現的行為中,它們的來源、使用和影響。4 _; t* [7 i3 W6 O& G5 g* g6 k* @' s
語用上的錯誤,如:
! Q# \: t* ]) P' E6 S: EdoSomething(); // ReferenceError: doSomething is not defined
/ N! Y1 K4 Q0 m- N$ {5 t; Q在這裡直接調用了一個未定義的函數,導致出錯。在一些其他場景中,雖然程序運行正確無誤,但是仍然可以優化(這種優化並不是技巧上的),比如:, f% D2 p8 `; \9 L
function doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();
3 J7 n# {; z8 o/ q9 ?4 A( J  在這裡,doSomethingElse 函數之前由於有 return,因此這個函數調用將永遠不能執行,這種冗余代碼對整個程序來說毫無用處,可以去掉。! w$ y. O5 N" C1 e8 r  ^
  對於 Closure Compiler 來說,它處理的對象是 js,不需要產生其他中間代碼或彙編代碼 / 機器碼,因此輸出的還是 js,但是是經過分析的、優化後的 js;另外,它也可以選擇輸出 parse tree(使用–print_tree 參數),所以,CC 的確完成了一個編譯器需要實現的功能。/ H- {" M, `) A# Z  i8 U/ b/ U! B
  CC 功能概述4 m- ?+ o* e9 w; @4 C3 M
  在詳細討論 CC 的高級模式前,還是簡明介紹一下功能體系。
1 @! c1 {$ x/ }) W; s8 ~$ Q1 l  編譯級別- `0 b0 ~" L/ y; O' D% H
  CC 的 compilation_level 包括三個級別:$ a  o" T3 [- i3 w& \& r1 u
WHITESPACE_ONLY3 C( t; G  v; K9 O7 a8 F
只刪除空白、註釋。
& n/ i8 S8 M: w6 z$ c( G4 P9 X8 w7 m" v$ @SIMPLE_OPTIMIZATIONS
) _9 W2 @; R3 `在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。0 Y' T4 x$ X  o, Z' h9 r% U7 |: D
ADVANCED_OPTIMIZATIONS
; @/ H" X8 ]' M3 n8 P3 T" w0 W) B; [: Y更加激進的重命名、移除垃圾代碼、內聯函數。1 i" m2 B+ G: l9 O- Z, V
  可以看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒做什麼真正的編譯工作,所以說,使用了高級模式的 CC 才是四肢健全的 CC 。
/ n# K, T) C/ u# m  約束條件5 [* Q' @. n+ W0 |7 b  D& Q5 ], D
  使用 CC 有一定約束條件,這影響到我們的編碼風格:
% K5 s3 i/ e* f* k' D# O7 w/ u0 wWHITESPACE_ONLY3 Z, H3 c; `# ?1 x9 F
不認可 JS 1.5 以上版本的語言特性' k2 h/ K# A: {) p; H) G9 J
不保留註釋
4 _/ K9 x, @  u$ B8 b* WSIMPLE_OPTIMIZATIONS
" ~6 `- c' @! t% z完全禁用 with 和 eval
; d- |! B! h5 M* ^( S2 I1 }' u: t: V: o字符串中引用的函數名 / 參數名不會改動(CC 不改動所有字符串)" w* R( x& Q. H$ F+ V% ~
ADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述' L+ d6 M3 {1 p3 R+ v
  註解- [& J6 f, t3 i& [9 Y
  Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。
) }! J  b. _" z" s7 k  使用 CC 高級模式3 D6 j- ^9 n4 V& {4 o. M- M
  在 CC 下,啟用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。
' {) B8 Z- Z" p7 N  作為一個 compiler,CC 的高級模式下,額外的優化政策是:
  r' v  x! O  |5 H% E# L' p/ k更激進的重命名,如 obj.property 改為 a.b,將深度過高的命名空間平坦化等;- N$ u1 y8 U: W7 o: S+ T' l+ Y
移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);2 a. U$ U: H3 x! Z
將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。- g; K5 Z' G% Q
  要達到高級模式的預期優化效果,開發者必須對自己做一些約束,因為 js 是弱類型、動態性的。否則' D- x! ~: u, `7 e$ T# @
js 的這種靈活將使 compiler 無能為力。
8 K0 X* ]9 D( n0 H  總體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。0 S' |& r9 v7 V; z8 V: s
  以下詳述具體的約束以及代碼的檢查 / 優化效果:
% X0 n7 Y  m3 J6 `- j  強類型的模擬
) A# T, C* n: Q+ S4 T2 x% q4 Z/ W@param 和 @type 中定義的類型會在編譯期間得到檢查,同樣避免了在運行時檢查,提高性能。
6 f2 Q6 `, y! |+ v* f% K, }3 g; ]@const 標記常量,當常量被寫時會報錯。
# j6 U6 w  w" H3 V; Q0 |- w模擬枚舉,將同類可枚舉常量定義為一個對像字面量,使用 @enum 標記:, s$ |/ H; l% @3 y5 `9 M2 E
var STATUS = { LOADING: 3, COMPLETE: 4};
; A6 t( b, r- n, ~( l9 H編譯結果中 STATUS.LOADING 會被直接替換為 3,其實完全模擬了 C 等語言中的枚舉。9 l2 ]: M$ _1 X  ]. I9 F+ u0 E! ?
使用 @constructor 標注函數為構造器,它僅能被實例化,而不可用作普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,否則報錯。這樣確保開發者不必在運行時判斷,構造器函數到底以怎樣的形式被調用。" i2 r% n1 e' F. M% r
在表達式中也可以使用 @type 來限定類型,這對於 JSON 特別有用,如
8 q; u& t$ ]* T) E! C* xvar data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'});2 R/ j" p7 |8 N. b
在這裡 UserModel 是個構造器,也可以使用 @typedef 來自定義複雜的數據類型。: e" _2 W/ k7 ~4 O$ g
  域可見性的模擬7 z+ q% p$ A# l  S
使用 @private 標注私有域,私有域被外部引用會報錯。開發者也可以按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒自己 / 協作者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者可以不必使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工作丟給CC,讓開發盡可能樸實簡單。6 l( \' U& T$ Y* ], z& Y' @
類似有 @protect
# @  ^4 s  r# c4 u. K# \# }  類系統的模擬1 u8 b! Z: ?& A/ g
使用 @extends 標注繼承關係,繼承體系會被優化。1 }# i0 G& g. D4 u4 f7 L
使用 @interface 標注接口,接口是類似 function ThisIsAInterface(obj) {}的函數體為空的構造器定義,編譯後將移除其相關代碼。同時,標注 @implements 的構造器必須實現implemented 的接口的所有方法(正如其他 OO 語言一樣),否則,CC 報錯。這同樣簡化了接口 / 實現的約束,靠 CC 來保證實現關係的可靠性。1 e# |1 D7 |. _6 o5 I
  條件編譯的模擬
5 b: Q! Z8 ^! B% F# m' {# N4 Z使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態需要分離的模式。
  ^1 }7 ^1 K5 l可以在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力……
+ h& H, B: x1 D  對像平坦化及屬性名縮減
/ b* i/ Z& {4 b+ ^5 `對像屬性會被編譯為單變量,比如 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~之後 foo$bar 被進一步縮短。對像之所以能被平坦化是因為在 js 中對象可以看做是一群引用 / 原始數據類型的容器。, z; o% i! ~% h" A* _
但是,js 對像實際上更複雜,所以被平坦化後會帶來一些副作用,比如如果在對像(字面量)中使用 this 指針,則編譯後的結果會導致 this 指向錯誤。所以 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味著,在所謂類單例(對像字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。9 t1 F, q( ^. r" C8 |
在縮減對像屬性 / 方法的名稱長度時,有另外一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是因為 CC 始終不處理字符串中的內容,所以,var o = { longName: 0 }; o["longName"] 會被翻譯為var a = { b: 0 }; a["longName"] 導致出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。6 f* d. v/ G3 c( n) ~( v
對於全局變量,如果出現以 window.property 的形式引用的,必須始終定義為 window.porperty 形式:
. @7 a: Q8 [1 F7 u8 H! R6 n2 _window.property = 1;var property = 1; // wrong!& T/ J% I: A8 T% f* r2 i
否則也會杯具,CC 可不會 window.property 翻譯為 window.a。
# w' q6 h/ k3 z8 _  垃圾代碼的移除
5 V1 N; M) o) V" e: {. X一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。
6 V) s8 M$ m& W在這種機制下,如果一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,因為這種動態特徵使得 CC 無法清楚方法是否確實在 for in 的時候被調用了。7 `8 O; H& S% M! I
對於一些 unreachable 的代碼,CC 將報警告。, A- h7 i4 ~) v" e! W: L' ]
如果要產出一份被調用的公共接口,例如庫,使用稱作 export 的方法將函數導出,防止函數定義被
2 m/ q4 e, F3 o' ]CC 回收。具體的做法是將函數綁定到某個容器,比如:
/ K9 h5 U: ]% ^0 xfunction displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;. x1 {: W7 f5 F5 b  \6 m, l
對於需要 export 的函數,均使用 quoted string 風格。, U/ ?' p5 G1 R, a  c1 ~9 ^  R
  背後的思考& \. v! _4 Q9 }; o! K
  根據以上高級模式優化的行為分析可知,CC 附加給開發者的約束主要有:9 R: C' }* b/ Z% e( l+ F9 b/ g
強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯
3 t. u2 Y- J5 c. \% `+ {, D+ y5 Z本身。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即通過開發者與 CC 達成一種" I# q/ u, Y. z
編碼約定而規避掉。: c; B( a( u7 c6 o$ V1 v
嚴格要求區分面向開發者的代碼和面向機器的代碼。
7 L% e9 m  V, U$ U雖然不像 C 等語言會編譯產生目標代碼,但是 CC 在一定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗余代碼去除。這和第一點其實是一脈相承的,同樣要求開發者將關注點轉移到開發本身。
% q+ j. r# }2 ]+ S使用規範化的接口方式。$ m8 j0 a2 h. S6 d1 P9 K+ E  Q7 L
這不僅包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用同樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,但是帶來的好處是明顯的:可讀、可控、一致性。& f4 {' o$ H; t4 E$ ^4 L" c7 u
  曾經有讀過 Closure Library 源碼的童鞋評論道:
5 v& Z1 z0 w7 O0 k( d2 q0 P* JGoogle 根本不懂怎麼寫 javascript!代碼裡面各種冗余,並且充滿了 java 的味道!
: ?; x. [2 r- o8 T8 f  當時確實也有這種感覺,比如 Google 把 if(foo) 寫作 if(foo != undefined) 等等。# `. R$ ?! }* k1 Y) @" {
  Javascript 固然充滿了豐富的動態特徵,而且很多特性非常優雅,能夠讓代碼簡潔精悍,或者構造出一些9 X* {7 V" W6 Z& i7 l$ Q) P
令人驚歎的技巧,但是也會產生一些副作用:
9 C/ R8 }- q' P首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西需要經過一番運算才能得出結論。4 M1 F$ ^+ B% J( Z$ H2 Q# I
比如 js 中的極晚綁定,再比如標識符運行時重寫。
' ]1 W6 C* p; a- Z' M4 y" e' H) o& H其次的問題是執行性能。一個比較經典的眾 js 工程師都在使用的技巧就是「模擬函數重載」——
: ?( ~8 ~! [/ Z7 d9 z在函數體內判斷 arguments 的特徵,從而對應給出不同的邏輯。由於缺乏強類型,js 本身不能具備
4 |* _" G$ Z7 U: K/ ~真正的重載,但是運行時的判斷在帶來靈活性的同時,必然會多出很多模擬重載的邏輯,降低性能。8 U. ]% ]: o+ d1 R5 [4 _* w
  在今年的 D2 大會上,Hedger 童鞋指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵作戰還可以,但是一旦碰到 army(軍隊,比如 Google 團隊這樣的 )就是個悲劇。
) l$ a# p1 ~  F% f! g2 q  我比較欣賞這個比喻,大團隊要良好地協作,必需遵循一定的規範和限制,優先保證可讀性和一致性,與此同時5 _- K  C- A  S7 y: q- o
失去的是奇技淫巧、自由靈活。所以採用何種編程風格、理念,需要具體問題具體分析…………
% \) c+ \8 l9 \5 {" F( s4 N% [  至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑒的。
3 Z# ?5 R& }- H# N# s$ t  最後附上 CC 的常用命令選項……選項實在是有夠多……' U+ `3 h2 T' l
  CC 常用命令選項( w" B8 f$ H4 W$ m) ?$ A0 U
–charset VAL 對所有文件定義的編碼格式
8 h8 a* D( v6 o; H/ s3 D–compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]
$ D; [2 F. F7 O% o- A* }: O% k設定編譯級別
- ~( k- U" S3 b+ k4 u3 Q–debug 開啟 debug 選項
$ l- G0 N+ y# H& T3 A5 v–define (–D, -D) VAL 設定文件中使用 @define 標注的開關值,即條件編譯
/ M+ Y  N! U0 {* g–externs VAL 編譯代碼需要調用未編譯的代碼時,使用它. q: Z% I" W) H& C
–formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出6 s" g" Q# y* r
–js VAL 輸入文件,多指定多個,將會被合併
# x2 D) S( p* ^9 t" @% l/ V$ k–jsoutputfile VAL 輸出文件,如果不指定的話,直接輸出到 standard output 流
' w+ `. C2 ]) k* x4 n! H–module VAL 定義模塊
! r2 b- ?& D" b; O. ~–output_manifest VAL 打印編譯文件清單" E% o( `4 d6 u* d$ C
–print_tree 打印語法分析樹  ^3 g& F6 M* j% \7 `
–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, 2025-7-16 05:07

By DZ X3.5

小黑屋

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