过期域名预定抢注

用戶名  找回密碼
 免费注册

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

[複製鏈接]
發表於 2011-7-10 09:23:36 | 顯示全部樓層 |閱讀模式
 本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考" |" J& x& X6 J
  CC 是真正的編譯器9 X* A! x) a' j* |9 z+ v; q3 }
  Closure Compiler 和 YUICompressor 並不是同類產品,雖然 CC 和 YC 同樣產出壓縮後的 JS 文件,但是 YC 只做了詞法上的掃瞄,而 CC 並不只是一個 compressor 那麼簡單,器如其名,它是一個compiler。" Q1 p( {* m) W+ |
  對於一個 compiler,一般地,它需要做到:
2 y% X  G, x" R4 y; o檢查源文本中語法、語義、語用上的錯誤;7 z2 y" l  C# h" h
根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;
6 ]" |6 F3 F# o5 ]7 ?& D& {優化。
8 w1 G  D, {7 v: L5 `) L  代碼錯誤一般來自三個方面:$ F7 x0 D# H% D& U
語法(Syntax)
0 I% J/ B& X) R- G/ G表示構成語言句子的各個記號之間的組合規律。大體上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……$ M1 _+ {' g9 m  n4 `! @; Z
語法上的錯誤,如:; f! |! R$ q3 |1 |- W  D4 U. c  L/ k
doSomething(;) // SyntaxError: Unexpected token ;
3 J4 c% S, @/ V; @( K" M根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並沒有配對),因此報錯。
  y! v( `3 k0 N語義(Semantics)
9 B6 i& k9 I7 W4 {% [表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 需要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。% H1 R  R7 s; _
語義上的錯誤,如:
: v3 P( `4 Z5 R0 = {}; // ReferenceError: Invalid left-hand side in assignment  O; f* o# \6 B& o; s$ L
根據賦值運算符 = 的意義,左操作數不能為字面量,所以雖然這個賦值語句包含了必需的左操作數、運算符、右操作數,仍然出錯。
- [- k8 f9 A6 Y語用(Pragmatics)+ _7 B/ a4 a5 ?1 O" m- T
表示在各個記號所出現的行為中,它們的來源、使用和影響。: L# j: k6 j0 w- L6 ?
語用上的錯誤,如:
  w8 P- ?9 ^! X, G/ ddoSomething(); // ReferenceError: doSomething is not defined# n) s; g, a/ _2 a% P9 ~- g  l: y
在這裡直接調用了一個未定義的函數,導致出錯。在一些其他場景中,雖然程序運行正確無誤,但是仍然可以優化(這種優化並不是技巧上的),比如:
. A' Q9 E6 x0 m3 \" {function doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();
. v! c% {/ |9 v' E  在這裡,doSomethingElse 函數之前由於有 return,因此這個函數調用將永遠不能執行,這種冗余代碼對整個程序來說毫無用處,可以去掉。
8 v, x  z& e' Y! B) M8 ]5 W* c  對於 Closure Compiler 來說,它處理的對象是 js,不需要產生其他中間代碼或彙編代碼 / 機器碼,因此輸出的還是 js,但是是經過分析的、優化後的 js;另外,它也可以選擇輸出 parse tree(使用–print_tree 參數),所以,CC 的確完成了一個編譯器需要實現的功能。9 m2 E/ s1 b% V5 o1 U+ O. i  p
  CC 功能概述  @- g- M. l. D. Q/ Z
  在詳細討論 CC 的高級模式前,還是簡明介紹一下功能體系。% x. l1 y' B+ H/ ]* b& {
  編譯級別$ U; C: L* Y; d2 }% f, o; K1 p
  CC 的 compilation_level 包括三個級別:( L9 K+ B$ U9 c2 N7 o
WHITESPACE_ONLY( S8 x4 E! Y( \; u
只刪除空白、註釋。- o, O+ t9 q# |
SIMPLE_OPTIMIZATIONS
* r/ ?; C  Z2 J, u$ k2 c0 ]! k在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。1 D3 X3 V; C' B
ADVANCED_OPTIMIZATIONS; A- r% j' N2 \/ h, R
更加激進的重命名、移除垃圾代碼、內聯函數。
' y8 ~  O7 A% M* y. U( Z  可以看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒做什麼真正的編譯工作,所以說,使用了高級模式的 CC 才是四肢健全的 CC 。. `' I# ~8 a& y. @: b8 i
  約束條件
! Q% Z4 k) c2 H# t  使用 CC 有一定約束條件,這影響到我們的編碼風格:
3 S* w& U' }& U4 H! ?WHITESPACE_ONLY6 O7 y; Z; }0 w+ {' |# c5 U
不認可 JS 1.5 以上版本的語言特性8 u( Y) E3 R' S: x, D  g6 [
不保留註釋. J' h- b4 ?# \# M6 j( @4 }3 j
SIMPLE_OPTIMIZATIONS
' W& M" [; o% H完全禁用 with 和 eval8 G0 P4 a8 R( t8 ~+ `
字符串中引用的函數名 / 參數名不會改動(CC 不改動所有字符串)0 e/ s9 D/ w6 t. k
ADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述
- b9 l2 j" D8 n. W  註解
: ~: o! q# p5 O1 P6 P- m- M+ h6 j  Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。
& q$ j/ c' {$ R8 F5 ^  使用 CC 高級模式
& j' v9 P; u+ q8 N) i" v  在 CC 下,啟用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。
* p. T) A- X; X6 \$ _1 W  作為一個 compiler,CC 的高級模式下,額外的優化政策是:5 [" i% `( N0 K6 r
更激進的重命名,如 obj.property 改為 a.b,將深度過高的命名空間平坦化等;; r3 J2 ^1 Y- B* S7 K
移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);
$ @' Z4 D; m! x5 d( m, t) y, e- p將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。
) }$ Q& p7 t& s+ ?9 R: n  要達到高級模式的預期優化效果,開發者必須對自己做一些約束,因為 js 是弱類型、動態性的。否則  C8 H+ S; [. l# w2 H' u
js 的這種靈活將使 compiler 無能為力。
8 V$ t7 a) g. l9 ]7 G, K& z  總體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。
# r# q' A- _0 ~' ?  以下詳述具體的約束以及代碼的檢查 / 優化效果:& H; h( k# K( \8 {7 t+ W. S6 H3 T3 J
  強類型的模擬$ B) `: K. [( c7 W8 \
@param 和 @type 中定義的類型會在編譯期間得到檢查,同樣避免了在運行時檢查,提高性能。; d6 l. ~3 ^! g* X) t
@const 標記常量,當常量被寫時會報錯。
4 Y1 c- v: w, |, l  i9 Y模擬枚舉,將同類可枚舉常量定義為一個對像字面量,使用 @enum 標記:
0 a# `9 }! A# D9 h1 j* I. W. J3 Wvar STATUS = { LOADING: 3, COMPLETE: 4};
; n: ~6 M# T# k2 P$ ~# |5 y編譯結果中 STATUS.LOADING 會被直接替換為 3,其實完全模擬了 C 等語言中的枚舉。* x/ `: h8 d3 n* K% _9 ~
使用 @constructor 標注函數為構造器,它僅能被實例化,而不可用作普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,否則報錯。這樣確保開發者不必在運行時判斷,構造器函數到底以怎樣的形式被調用。* f% x9 F$ r* W( J' w2 P* T. J
在表達式中也可以使用 @type 來限定類型,這對於 JSON 特別有用,如- i5 w, ~  B0 Z& b- Y9 L
var data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'});
# l2 T' Z9 R$ d5 k/ e在這裡 UserModel 是個構造器,也可以使用 @typedef 來自定義複雜的數據類型。
! |3 R+ G- k) Z4 P  _4 V  域可見性的模擬
8 d4 i; _5 e  }2 z* C7 a, V& Z使用 @private 標注私有域,私有域被外部引用會報錯。開發者也可以按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒自己 / 協作者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者可以不必使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工作丟給CC,讓開發盡可能樸實簡單。
2 {- Z# g" G$ i% g  v$ v0 T3 K類似有 @protect
9 a4 O( v2 D7 e' T! r2 L, L  類系統的模擬0 A" Q: m; G) v3 Z: U  s% ?
使用 @extends 標注繼承關係,繼承體系會被優化。
# Y' W& w/ M2 P) e* `使用 @interface 標注接口,接口是類似 function ThisIsAInterface(obj) {}的函數體為空的構造器定義,編譯後將移除其相關代碼。同時,標注 @implements 的構造器必須實現implemented 的接口的所有方法(正如其他 OO 語言一樣),否則,CC 報錯。這同樣簡化了接口 / 實現的約束,靠 CC 來保證實現關係的可靠性。
: J& H/ @) X& q4 K  條件編譯的模擬6 ~* S! k# ~8 s& `7 v, x9 W
使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態需要分離的模式。2 b7 `9 D2 t3 {* c
可以在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力……
. V) g* ~# W2 X0 J- x% s  對像平坦化及屬性名縮減4 j+ m  L+ q! q7 d$ O& |/ c
對像屬性會被編譯為單變量,比如 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~之後 foo$bar 被進一步縮短。對像之所以能被平坦化是因為在 js 中對象可以看做是一群引用 / 原始數據類型的容器。
$ F. E8 }( {% N+ m: l+ c/ g, y但是,js 對像實際上更複雜,所以被平坦化後會帶來一些副作用,比如如果在對像(字面量)中使用 this 指針,則編譯後的結果會導致 this 指向錯誤。所以 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味著,在所謂類單例(對像字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。
; ~! {  F  A4 a2 Y- v6 o6 N在縮減對像屬性 / 方法的名稱長度時,有另外一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是因為 CC 始終不處理字符串中的內容,所以,var o = { longName: 0 }; o["longName"] 會被翻譯為var a = { b: 0 }; a["longName"] 導致出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。
, [* N' r& H; o. Q對於全局變量,如果出現以 window.property 的形式引用的,必須始終定義為 window.porperty 形式:
, c% l0 l5 G7 k. vwindow.property = 1;var property = 1; // wrong!
  k% @( c6 H' C' @. ~否則也會杯具,CC 可不會 window.property 翻譯為 window.a。4 F  y2 {2 h: x) Y( [
  垃圾代碼的移除
' ^& J2 q2 Z8 R$ ?一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。
) f' e* s! I0 t0 V% Y在這種機制下,如果一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,因為這種動態特徵使得 CC 無法清楚方法是否確實在 for in 的時候被調用了。1 d% h2 C  ]3 P+ V6 E
對於一些 unreachable 的代碼,CC 將報警告。
( F8 w# A- j6 q5 m如果要產出一份被調用的公共接口,例如庫,使用稱作 export 的方法將函數導出,防止函數定義被$ f' ]( K  E) Q5 @. l6 F% B
CC 回收。具體的做法是將函數綁定到某個容器,比如:
$ A* O9 T4 r  i2 pfunction displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;
) C: Z* Z* q# l) u$ s8 d0 {對於需要 export 的函數,均使用 quoted string 風格。
3 d" F& T8 w9 |4 Q6 h- n  背後的思考4 I' E( d/ V1 L( ^- b
  根據以上高級模式優化的行為分析可知,CC 附加給開發者的約束主要有:8 O6 Q6 D% v. {: \6 h
強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯
# D* @! Q( R7 n, P: Y- e本身。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即通過開發者與 CC 達成一種
" l8 _6 P. }  O編碼約定而規避掉。5 l7 I# y; m" A' G; O/ b
嚴格要求區分面向開發者的代碼和面向機器的代碼。8 H! T7 N5 `# [" y0 k& K) ?
雖然不像 C 等語言會編譯產生目標代碼,但是 CC 在一定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗余代碼去除。這和第一點其實是一脈相承的,同樣要求開發者將關注點轉移到開發本身。: u% Y% j4 U0 q6 `1 ~. S8 B9 t, |
使用規範化的接口方式。
6 A! ~' @  Z6 l% C. O& l) E, U6 s0 r這不僅包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用同樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,但是帶來的好處是明顯的:可讀、可控、一致性。
6 w% o# f0 y4 l5 R  曾經有讀過 Closure Library 源碼的童鞋評論道:4 j/ {2 z# l0 k! _8 B4 l  N# ], D
Google 根本不懂怎麼寫 javascript!代碼裡面各種冗余,並且充滿了 java 的味道!! K5 A3 g. q2 x! T  X
  當時確實也有這種感覺,比如 Google 把 if(foo) 寫作 if(foo != undefined) 等等。4 j2 p' S0 M( E# D4 C
  Javascript 固然充滿了豐富的動態特徵,而且很多特性非常優雅,能夠讓代碼簡潔精悍,或者構造出一些/ p* W6 s4 [- ~/ ^1 M
令人驚歎的技巧,但是也會產生一些副作用:
' h2 p0 ~7 u, O0 w首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西需要經過一番運算才能得出結論。
2 i1 @3 A8 |, p/ R9 E& z) K比如 js 中的極晚綁定,再比如標識符運行時重寫。1 x7 {$ L! ^# ?% _4 U
其次的問題是執行性能。一個比較經典的眾 js 工程師都在使用的技巧就是「模擬函數重載」——
& s$ I0 h1 W% V7 A& }$ |( ]在函數體內判斷 arguments 的特徵,從而對應給出不同的邏輯。由於缺乏強類型,js 本身不能具備. M4 Y! A; R1 ]
真正的重載,但是運行時的判斷在帶來靈活性的同時,必然會多出很多模擬重載的邏輯,降低性能。. J' \5 r* D4 r+ w
  在今年的 D2 大會上,Hedger 童鞋指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵作戰還可以,但是一旦碰到 army(軍隊,比如 Google 團隊這樣的 )就是個悲劇。% I3 J4 X5 c- m
  我比較欣賞這個比喻,大團隊要良好地協作,必需遵循一定的規範和限制,優先保證可讀性和一致性,與此同時
) u% O8 w$ T# M7 E3 t5 \2 K失去的是奇技淫巧、自由靈活。所以採用何種編程風格、理念,需要具體問題具體分析…………
/ R* I- I; X* I: j, b1 j' }: [  至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑒的。, ?% `2 r1 F- b& Z$ Z2 r; w# s
  最後附上 CC 的常用命令選項……選項實在是有夠多……
' h) J7 m6 s, B1 C, }( n7 u  CC 常用命令選項; v' ]+ U% _5 ?
–charset VAL 對所有文件定義的編碼格式
' M* |: a0 y9 M% k% f& }–compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]! ^, y8 t- U( F; z! U
設定編譯級別+ l# F+ t0 u+ b, W/ w
–debug 開啟 debug 選項$ z9 I+ S! l! n& i4 e) I
–define (–D, -D) VAL 設定文件中使用 @define 標注的開關值,即條件編譯2 r# |& `0 S* S# ^. Z4 K6 l
–externs VAL 編譯代碼需要調用未編譯的代碼時,使用它
3 D4 k, x9 J4 j4 `; s( K: G–formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出8 \9 e- r" ]" v
–js VAL 輸入文件,多指定多個,將會被合併
7 o+ ?. H6 U  Y$ k–jsoutputfile VAL 輸出文件,如果不指定的話,直接輸出到 standard output 流! V( B5 E" n5 n
–module VAL 定義模塊
! x# q# s3 ]$ D7 y8 T8 a–output_manifest VAL 打印編譯文件清單
! @% y8 y& F0 t' F–print_tree 打印語法分析樹
) M, i2 ?; H3 F# d1 ]–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-5-16 20:58

By DZ X3.5

小黑屋

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