过期域名预定抢注

 找回密碼
 免费注册

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

[複製鏈接]
發表於 2011-7-10 09:23:36 | 顯示全部樓層 |閱讀模式
 本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考
. }3 `* _# s5 T- X9 j9 t4 ~" b  CC 是真正的編譯器% o2 e9 z  W7 |& E
  Closure Compiler 和 YUICompressor 並不是同類產品,雖然 CC 和 YC 同樣產出壓縮後的 JS 文件,但是 YC 只做了詞法上的掃瞄,而 CC 並不只是一個 compressor 那麼簡單,器如其名,它是一個compiler。+ D9 Y5 S/ a+ m; v  ]! p4 ~
  對於一個 compiler,一般地,它需要做到:+ P0 _; u0 C( T+ B4 a+ S1 P1 W
檢查源文本中語法、語義、語用上的錯誤;, E5 P7 X  E* }9 o. ]
根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;. p/ W. B1 e  S/ Z0 h# @2 ^8 z; J
優化。- j: ^  @: r0 P2 C$ L
  代碼錯誤一般來自三個方面:( G) |8 ?3 w5 E- ]. k0 {# Q! ?3 Q" f
語法(Syntax); E2 g0 b  B  x. v9 f; s6 x
表示構成語言句子的各個記號之間的組合規律。大體上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……
8 u$ t0 A# i! ^: D& m! H: o語法上的錯誤,如:
1 B9 n: ?9 N& q3 i8 {doSomething(;) // SyntaxError: Unexpected token ;6 I6 Q1 {+ a- v
根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並沒有配對),因此報錯。
. E$ C) R& P3 v- i4 n1 {# u) F語義(Semantics)
) m; k( A0 A( C表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 需要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。
! R% ^# x" q5 t1 g5 e5 Y語義上的錯誤,如:) v9 W, J5 m0 I3 v- g
0 = {}; // ReferenceError: Invalid left-hand side in assignment
; [6 l! K, C8 z7 h4 v根據賦值運算符 = 的意義,左操作數不能為字面量,所以雖然這個賦值語句包含了必需的左操作數、運算符、右操作數,仍然出錯。
0 O9 \9 A, N) H0 L6 s+ i語用(Pragmatics)
* ?3 B( C$ T9 M3 k! _& M/ x6 m表示在各個記號所出現的行為中,它們的來源、使用和影響。
2 C2 h% H& e, c: J% s+ [+ d語用上的錯誤,如:
; h+ d! L9 x; Y$ G5 ?- B; o: S2 t; TdoSomething(); // ReferenceError: doSomething is not defined
- M" {, y/ {2 |4 U) U! l; \& I在這裡直接調用了一個未定義的函數,導致出錯。在一些其他場景中,雖然程序運行正確無誤,但是仍然可以優化(這種優化並不是技巧上的),比如:
! D9 d7 L+ x3 Lfunction doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();) z- Y# v. ?) f
  在這裡,doSomethingElse 函數之前由於有 return,因此這個函數調用將永遠不能執行,這種冗余代碼對整個程序來說毫無用處,可以去掉。6 i, T; o8 R; u' P+ h, N8 |# @
  對於 Closure Compiler 來說,它處理的對象是 js,不需要產生其他中間代碼或彙編代碼 / 機器碼,因此輸出的還是 js,但是是經過分析的、優化後的 js;另外,它也可以選擇輸出 parse tree(使用–print_tree 參數),所以,CC 的確完成了一個編譯器需要實現的功能。" \% G) `2 }3 U1 H8 A' V
  CC 功能概述1 ^9 ~# J" A/ b9 u( a) f4 V* e
  在詳細討論 CC 的高級模式前,還是簡明介紹一下功能體系。
. j4 g& S8 Q7 r  編譯級別
# h# k; H+ r/ h, v7 ~. _4 U# t7 E  CC 的 compilation_level 包括三個級別:
' u% r% ], M; w' NWHITESPACE_ONLY
: [' r3 ~3 x+ D) K! J) o只刪除空白、註釋。
! D9 D8 q) D2 J1 \SIMPLE_OPTIMIZATIONS
3 C7 R+ J3 e" f% r在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。
' N4 q1 W" |9 ~" W& yADVANCED_OPTIMIZATIONS
9 e' G, {5 R: ?3 w6 S更加激進的重命名、移除垃圾代碼、內聯函數。
3 K) L% F* w1 F& o  可以看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒做什麼真正的編譯工作,所以說,使用了高級模式的 CC 才是四肢健全的 CC 。
# l; n. M) Y( T, v% F! S$ F3 }  約束條件
5 X. a+ A8 W' d$ a" T& Q" l  使用 CC 有一定約束條件,這影響到我們的編碼風格:
# B1 y9 G% s" f$ t/ y6 QWHITESPACE_ONLY9 R8 ~* N) m' S
不認可 JS 1.5 以上版本的語言特性5 u$ _6 ]/ P$ O* X
不保留註釋; Z% Z6 I3 E" W' N
SIMPLE_OPTIMIZATIONS9 Z) k# T" [2 b  H  c7 X
完全禁用 with 和 eval
5 Y! t' o1 r+ Y" S: \5 V! L字符串中引用的函數名 / 參數名不會改動(CC 不改動所有字符串)3 T% y5 v4 |. D- q* f+ v' h4 q0 `
ADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述
( [7 X) v8 l4 }3 {  註解' [& t2 j9 d& Z0 y6 m8 H' U# J
  Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。
  t6 G* X! {) h6 c6 ?  [  使用 CC 高級模式6 H; G2 O# u  f+ C2 O
  在 CC 下,啟用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。: O+ L' Y1 g4 a' l8 ^' ^1 ]
  作為一個 compiler,CC 的高級模式下,額外的優化政策是:$ A, k7 s5 T- D  J
更激進的重命名,如 obj.property 改為 a.b,將深度過高的命名空間平坦化等;
; `# i" o; i! y. R$ i  Z* k, u% R- l移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);
$ I, g5 h, k: D9 g將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。' Z0 a6 P2 ^: e+ y  s
  要達到高級模式的預期優化效果,開發者必須對自己做一些約束,因為 js 是弱類型、動態性的。否則* T! g: K+ x7 V5 |
js 的這種靈活將使 compiler 無能為力。' Z' Z% v4 A$ z: }+ ~
  總體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。7 n6 ?0 X' b5 N3 t
  以下詳述具體的約束以及代碼的檢查 / 優化效果:
& ~" i, S& n( p7 V5 E! e  強類型的模擬2 U# O' H% t4 U$ J. R0 M+ A
@param 和 @type 中定義的類型會在編譯期間得到檢查,同樣避免了在運行時檢查,提高性能。
# A* r. N& v7 l5 v; B& D@const 標記常量,當常量被寫時會報錯。& h3 c- F1 Z3 k' ]5 t9 S
模擬枚舉,將同類可枚舉常量定義為一個對像字面量,使用 @enum 標記:8 C7 \; R  H) Y/ o% j9 |9 c; C
var STATUS = { LOADING: 3, COMPLETE: 4};. I" ^$ G1 C  X9 l
編譯結果中 STATUS.LOADING 會被直接替換為 3,其實完全模擬了 C 等語言中的枚舉。
% O# S8 [' N, @0 A" e使用 @constructor 標注函數為構造器,它僅能被實例化,而不可用作普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,否則報錯。這樣確保開發者不必在運行時判斷,構造器函數到底以怎樣的形式被調用。
) `) j( V  y8 p3 l/ A+ g' F在表達式中也可以使用 @type 來限定類型,這對於 JSON 特別有用,如& Q( L9 H; e% R' ~8 k
var data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'});4 m/ `/ H- N2 y
在這裡 UserModel 是個構造器,也可以使用 @typedef 來自定義複雜的數據類型。
7 k: ?  W$ S. X# f; V  域可見性的模擬
0 J: l# M. T/ y' j0 u0 j使用 @private 標注私有域,私有域被外部引用會報錯。開發者也可以按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒自己 / 協作者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者可以不必使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工作丟給CC,讓開發盡可能樸實簡單。% m, e$ A% t6 J# D
類似有 @protect
" B" \& k9 n) e6 z+ M; s& E  類系統的模擬& u8 p3 t0 a* H! z, l
使用 @extends 標注繼承關係,繼承體系會被優化。- G& Y- T! m$ o  j. }
使用 @interface 標注接口,接口是類似 function ThisIsAInterface(obj) {}的函數體為空的構造器定義,編譯後將移除其相關代碼。同時,標注 @implements 的構造器必須實現implemented 的接口的所有方法(正如其他 OO 語言一樣),否則,CC 報錯。這同樣簡化了接口 / 實現的約束,靠 CC 來保證實現關係的可靠性。* W3 ^' J2 h* V+ x, u) G
  條件編譯的模擬9 R5 j9 w) A) {1 }: D
使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態需要分離的模式。
: C6 m+ [: ]# B6 a6 Y% u6 \7 A可以在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力……7 T" r- F" m5 r
  對像平坦化及屬性名縮減& @, k+ F2 z9 A( V- e6 T
對像屬性會被編譯為單變量,比如 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~之後 foo$bar 被進一步縮短。對像之所以能被平坦化是因為在 js 中對象可以看做是一群引用 / 原始數據類型的容器。
8 `; v8 h2 X% W0 N但是,js 對像實際上更複雜,所以被平坦化後會帶來一些副作用,比如如果在對像(字面量)中使用 this 指針,則編譯後的結果會導致 this 指向錯誤。所以 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味著,在所謂類單例(對像字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。, ~" y5 s& ?! m9 f$ C
在縮減對像屬性 / 方法的名稱長度時,有另外一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是因為 CC 始終不處理字符串中的內容,所以,var o = { longName: 0 }; o["longName"] 會被翻譯為var a = { b: 0 }; a["longName"] 導致出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。
6 Y8 l& u; {* x+ O5 S% L: b0 c& w對於全局變量,如果出現以 window.property 的形式引用的,必須始終定義為 window.porperty 形式:2 B+ z/ \2 `1 A0 e
window.property = 1;var property = 1; // wrong!
) ]/ m9 r$ _4 e/ t9 d1 e否則也會杯具,CC 可不會 window.property 翻譯為 window.a。$ v0 M! U4 j9 A
  垃圾代碼的移除
8 M/ v/ m/ Z0 H1 D3 Y8 l3 J一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。
% R% W, @  f$ L1 s5 u  q在這種機制下,如果一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,因為這種動態特徵使得 CC 無法清楚方法是否確實在 for in 的時候被調用了。. n* w8 T. \. [. L  ]
對於一些 unreachable 的代碼,CC 將報警告。) O3 t7 Q( w! Z: w  Z9 Y
如果要產出一份被調用的公共接口,例如庫,使用稱作 export 的方法將函數導出,防止函數定義被6 T# U7 \7 C7 _& H* r
CC 回收。具體的做法是將函數綁定到某個容器,比如:0 |/ t2 T+ t- F
function displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;' Q& m# p/ b" G' G5 [9 L5 {2 X: i1 d
對於需要 export 的函數,均使用 quoted string 風格。1 i( l4 }: D. g$ M7 e8 o$ I4 e
  背後的思考
. ^/ s  P9 d% M" C6 r  根據以上高級模式優化的行為分析可知,CC 附加給開發者的約束主要有:
! f. s3 h! T: S4 L% j強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯
! L$ l- C# h  g本身。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即通過開發者與 CC 達成一種6 E' n6 N$ B6 w! f
編碼約定而規避掉。! ^9 \# g5 r- ]3 T
嚴格要求區分面向開發者的代碼和面向機器的代碼。1 g! b! G  D, w2 L4 Z
雖然不像 C 等語言會編譯產生目標代碼,但是 CC 在一定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗余代碼去除。這和第一點其實是一脈相承的,同樣要求開發者將關注點轉移到開發本身。1 I& }" V6 U2 S# w0 A# r$ h
使用規範化的接口方式。0 b# m( C  f. z8 r
這不僅包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用同樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,但是帶來的好處是明顯的:可讀、可控、一致性。
' c0 t9 H/ p( Y2 P  曾經有讀過 Closure Library 源碼的童鞋評論道:4 E7 G9 ?  s/ E
Google 根本不懂怎麼寫 javascript!代碼裡面各種冗余,並且充滿了 java 的味道!
/ D% U9 H, E5 A: u% ]& l0 L( ?  當時確實也有這種感覺,比如 Google 把 if(foo) 寫作 if(foo != undefined) 等等。
1 R3 @. m' t9 O9 V+ C9 T; k  Javascript 固然充滿了豐富的動態特徵,而且很多特性非常優雅,能夠讓代碼簡潔精悍,或者構造出一些  x/ J' f% B, V/ z5 F" v
令人驚歎的技巧,但是也會產生一些副作用:* U( u. Q) k$ K0 [, c0 O2 m
首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西需要經過一番運算才能得出結論。
- Q3 X! s. J* U: a比如 js 中的極晚綁定,再比如標識符運行時重寫。
+ L7 Q8 V) ]/ b其次的問題是執行性能。一個比較經典的眾 js 工程師都在使用的技巧就是「模擬函數重載」——
0 n7 Y, I6 `& z$ c4 {! u在函數體內判斷 arguments 的特徵,從而對應給出不同的邏輯。由於缺乏強類型,js 本身不能具備" m7 m; l! P& Z1 D
真正的重載,但是運行時的判斷在帶來靈活性的同時,必然會多出很多模擬重載的邏輯,降低性能。5 ?* `: i4 g- i* o7 e' L4 t6 H
  在今年的 D2 大會上,Hedger 童鞋指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵作戰還可以,但是一旦碰到 army(軍隊,比如 Google 團隊這樣的 )就是個悲劇。6 i: E# N1 L0 Q) d
  我比較欣賞這個比喻,大團隊要良好地協作,必需遵循一定的規範和限制,優先保證可讀性和一致性,與此同時; Y$ i3 C+ w5 N6 |# e' c
失去的是奇技淫巧、自由靈活。所以採用何種編程風格、理念,需要具體問題具體分析…………4 |9 z, g8 q7 N' R: O7 p
  至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑒的。5 R; L* q( u- N! [  U- R( h
  最後附上 CC 的常用命令選項……選項實在是有夠多……& ~- u: F# H7 X  W! @3 H- `; Y3 I  f
  CC 常用命令選項9 w# w' E1 B8 D+ I- C; x8 j
–charset VAL 對所有文件定義的編碼格式
9 T, n1 i+ ]6 P8 ~) N' M–compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]
7 n3 ^$ Y& f" v設定編譯級別
% a0 g' K+ [4 b) T–debug 開啟 debug 選項/ ?3 f! u/ I7 T8 P& G+ q8 c
–define (–D, -D) VAL 設定文件中使用 @define 標注的開關值,即條件編譯4 \' l" Z; S" S0 ?9 R0 _# k
–externs VAL 編譯代碼需要調用未編譯的代碼時,使用它( C  a: [# Q( r" m: H* W  q
–formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出
# W: V/ G  }$ v9 N) a) J  D' _& I–js VAL 輸入文件,多指定多個,將會被合併# L" G' e6 l  A. Q3 s7 v% G
–jsoutputfile VAL 輸出文件,如果不指定的話,直接輸出到 standard output 流& `, \$ S6 m3 ^" W1 t( L
–module VAL 定義模塊
: J* r3 ^7 N4 k5 U$ B6 G+ y–output_manifest VAL 打印編譯文件清單
, w* ~7 L8 R( j+ k& `6 r9 W: [( P–print_tree 打印語法分析樹
. X; e+ X$ K* X, o, S  x–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-9-16 14:01

By DZ X3.5

小黑屋

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