过期域名预定抢注

 找回密碼
 免费注册

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

[複製鏈接]
發表於 2011-7-10 09:23:36 | 顯示全部樓層 |閱讀模式
 本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考
  H) e0 J( E; Y' J6 o" i  CC 是真正的編譯器8 \6 k4 x2 s$ `, V$ p
  Closure Compiler 和 YUICompressor 並不是同類產品,雖然 CC 和 YC 同樣產出壓縮後的 JS 文件,但是 YC 只做了詞法上的掃瞄,而 CC 並不只是一個 compressor 那麼簡單,器如其名,它是一個compiler。
7 P4 U; M5 r! p& u! r, h  對於一個 compiler,一般地,它需要做到:2 ^  Y' n0 M( M% t' g
檢查源文本中語法、語義、語用上的錯誤;
% r! ?% L! p  y- [根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;1 i- V! ?, L( M$ Z0 G) p
優化。
; L! f' p7 Z9 X+ `1 L5 _- X  代碼錯誤一般來自三個方面:
$ f/ |) _( @" a, ~3 t* y/ [# ~+ R語法(Syntax)
: J% |% W/ T) d" R' x/ x/ o# h, Z表示構成語言句子的各個記號之間的組合規律。大體上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……. N0 ?! F& m7 j; z# G$ I5 @9 D" j( x% s4 c
語法上的錯誤,如:/ c* T1 H9 J* b0 f* _4 Y
doSomething(;) // SyntaxError: Unexpected token ;# {) l9 l9 H; {, D
根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並沒有配對),因此報錯。
+ m1 A$ A3 q4 {1 }語義(Semantics)
$ q( E5 O2 x- |& v! @5 t, ]表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 需要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。& g( J: h2 v& `' \; m1 c7 u7 h
語義上的錯誤,如:* Q/ G# x5 |! Z9 I3 ^: H$ h
0 = {}; // ReferenceError: Invalid left-hand side in assignment
8 L/ k3 ~/ z2 V, E0 j2 U根據賦值運算符 = 的意義,左操作數不能為字面量,所以雖然這個賦值語句包含了必需的左操作數、運算符、右操作數,仍然出錯。0 V% Y; q: C; P  {
語用(Pragmatics)
; k6 R5 m1 z8 Z- w4 T7 m1 o6 S, ]5 f表示在各個記號所出現的行為中,它們的來源、使用和影響。
& a3 K. u+ B5 S! z: Z$ K語用上的錯誤,如:, @, N8 c8 D+ U/ ^! Q+ i
doSomething(); // ReferenceError: doSomething is not defined1 s: a/ r) Q4 E! F' f
在這裡直接調用了一個未定義的函數,導致出錯。在一些其他場景中,雖然程序運行正確無誤,但是仍然可以優化(這種優化並不是技巧上的),比如:7 p) ]" I+ ~/ v; V4 A
function doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();2 y0 G# v/ c0 f8 t! O3 ?
  在這裡,doSomethingElse 函數之前由於有 return,因此這個函數調用將永遠不能執行,這種冗余代碼對整個程序來說毫無用處,可以去掉。: k# A: J7 F5 G5 y
  對於 Closure Compiler 來說,它處理的對象是 js,不需要產生其他中間代碼或彙編代碼 / 機器碼,因此輸出的還是 js,但是是經過分析的、優化後的 js;另外,它也可以選擇輸出 parse tree(使用–print_tree 參數),所以,CC 的確完成了一個編譯器需要實現的功能。
5 s$ h) M3 |; x4 e  CC 功能概述1 q: t+ y- l# q  u. J
  在詳細討論 CC 的高級模式前,還是簡明介紹一下功能體系。
( q- e, J: t: H; [. \  k  編譯級別
  }2 ^0 c/ t) w2 f5 g0 C  CC 的 compilation_level 包括三個級別:: u" x9 i/ {+ B9 {, \* d
WHITESPACE_ONLY+ b+ W2 ]) f2 ^5 n! O
只刪除空白、註釋。
7 E7 j) ?/ C- Y! l. q* {0 h/ p/ x$ ISIMPLE_OPTIMIZATIONS
1 P# T9 c9 z0 `. n( m3 C! B* m! X在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。4 o- M4 r( }* N& I& m( {
ADVANCED_OPTIMIZATIONS+ h  p+ T7 v  F' D
更加激進的重命名、移除垃圾代碼、內聯函數。1 c" l; [! g3 P* N6 g# T2 i
  可以看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒做什麼真正的編譯工作,所以說,使用了高級模式的 CC 才是四肢健全的 CC 。
% _- c( U* q7 K# _2 I  約束條件) ?& ~& k9 S* K2 ~6 Z0 q5 P
  使用 CC 有一定約束條件,這影響到我們的編碼風格:
6 w6 x$ {" l% `* O/ {! ?2 p5 o( GWHITESPACE_ONLY
2 E6 n7 z, o. r/ \不認可 JS 1.5 以上版本的語言特性# _! R, D7 r+ c; Z$ v
不保留註釋# f8 f# x- w2 [4 ]2 @
SIMPLE_OPTIMIZATIONS1 k  G2 e1 e. R8 O; l5 q; S
完全禁用 with 和 eval
% n7 ^& x# A+ j( k$ u2 e字符串中引用的函數名 / 參數名不會改動(CC 不改動所有字符串)
6 @) H" |+ C3 }" P0 `ADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述
4 K$ \- T  L" n* f" n/ N; s( B  註解6 B: P# p" b' Q4 f- z
  Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。
3 X0 Q; A( e2 C9 \9 W  使用 CC 高級模式
4 ~$ F6 H4 K5 G% j# ~# ~1 r  在 CC 下,啟用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。
; s3 ?1 P& Y7 h8 I0 q% k4 x  作為一個 compiler,CC 的高級模式下,額外的優化政策是:- _& l1 T) `7 k
更激進的重命名,如 obj.property 改為 a.b,將深度過高的命名空間平坦化等;: C* v, m+ f3 x4 v
移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);
8 d( {9 Y+ L' e6 v5 l: B) f2 h將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。
% Z% {  @% i: v4 O4 g; D: e  要達到高級模式的預期優化效果,開發者必須對自己做一些約束,因為 js 是弱類型、動態性的。否則+ W: @' `, ?& x- c- H/ i* c* F
js 的這種靈活將使 compiler 無能為力。
$ [9 s: |0 T: r8 R& X  總體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。
) x# J; v. P$ i6 o7 C& p5 T* {  以下詳述具體的約束以及代碼的檢查 / 優化效果:
# `7 K: Z; m1 k+ U. Y  強類型的模擬
8 r: Q7 _; X  q@param 和 @type 中定義的類型會在編譯期間得到檢查,同樣避免了在運行時檢查,提高性能。
; {# [) D$ ^  `/ S0 i@const 標記常量,當常量被寫時會報錯。
) F" t! ~  z  C7 U; Q模擬枚舉,將同類可枚舉常量定義為一個對像字面量,使用 @enum 標記:; r1 n' o+ f! ^/ \+ b' H
var STATUS = { LOADING: 3, COMPLETE: 4};% r$ [5 {; E4 R. ]" n
編譯結果中 STATUS.LOADING 會被直接替換為 3,其實完全模擬了 C 等語言中的枚舉。
7 r/ x/ S! ^1 \8 {4 L0 ~使用 @constructor 標注函數為構造器,它僅能被實例化,而不可用作普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,否則報錯。這樣確保開發者不必在運行時判斷,構造器函數到底以怎樣的形式被調用。
! L) e3 |' ?- b4 {8 i( k3 v* V# {在表達式中也可以使用 @type 來限定類型,這對於 JSON 特別有用,如
# G2 K5 P, X" f. C! U  {, A" S' fvar data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'});: h9 g/ O2 s2 L, c
在這裡 UserModel 是個構造器,也可以使用 @typedef 來自定義複雜的數據類型。
1 x* ?$ ~3 o0 r1 L0 u# y9 M7 S  域可見性的模擬
  q$ v7 V" U7 m, w3 Y( @使用 @private 標注私有域,私有域被外部引用會報錯。開發者也可以按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒自己 / 協作者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者可以不必使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工作丟給CC,讓開發盡可能樸實簡單。
5 U/ \8 m5 p" B# i2 U5 s5 p類似有 @protect
  G8 t" M$ E" J% e  類系統的模擬/ `* G- L$ p6 ?( @2 S6 ~: I0 d0 k% G
使用 @extends 標注繼承關係,繼承體系會被優化。+ C' f' i6 }- k) K! a6 |/ @# o
使用 @interface 標注接口,接口是類似 function ThisIsAInterface(obj) {}的函數體為空的構造器定義,編譯後將移除其相關代碼。同時,標注 @implements 的構造器必須實現implemented 的接口的所有方法(正如其他 OO 語言一樣),否則,CC 報錯。這同樣簡化了接口 / 實現的約束,靠 CC 來保證實現關係的可靠性。* ~8 t4 w" G6 m' e' H5 E
  條件編譯的模擬
$ X( ^5 ~% d# u( T+ I使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態需要分離的模式。% R* K. s1 n3 r7 p! T9 R
可以在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力……" |/ q& l5 v' _+ V/ \- _4 f; T
  對像平坦化及屬性名縮減
* K6 a) P  y$ u8 T, h+ [$ z. t& Z對像屬性會被編譯為單變量,比如 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~之後 foo$bar 被進一步縮短。對像之所以能被平坦化是因為在 js 中對象可以看做是一群引用 / 原始數據類型的容器。4 z9 p* x; K- j: f) q/ {
但是,js 對像實際上更複雜,所以被平坦化後會帶來一些副作用,比如如果在對像(字面量)中使用 this 指針,則編譯後的結果會導致 this 指向錯誤。所以 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味著,在所謂類單例(對像字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。  {+ m* i; x. H% E' ?
在縮減對像屬性 / 方法的名稱長度時,有另外一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是因為 CC 始終不處理字符串中的內容,所以,var o = { longName: 0 }; o["longName"] 會被翻譯為var a = { b: 0 }; a["longName"] 導致出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。" X) W$ k& U# i  h  o; w
對於全局變量,如果出現以 window.property 的形式引用的,必須始終定義為 window.porperty 形式:+ Z) k6 g% t( v$ L
window.property = 1;var property = 1; // wrong!
- u* {' ]5 p" i7 X8 f' S% Q否則也會杯具,CC 可不會 window.property 翻譯為 window.a。
4 j( _* L! B5 Q& I/ `5 w5 i$ Y7 V  垃圾代碼的移除
% o6 _" y0 W) {9 Q- ]6 m9 E一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。5 Z. x, ~* g- n2 `( r* p" I" e2 M' ]1 D
在這種機制下,如果一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,因為這種動態特徵使得 CC 無法清楚方法是否確實在 for in 的時候被調用了。: ]/ m$ N% K% c# ]% Z6 l& A0 w
對於一些 unreachable 的代碼,CC 將報警告。
2 s7 i' |* D7 H, ?如果要產出一份被調用的公共接口,例如庫,使用稱作 export 的方法將函數導出,防止函數定義被
! M4 v9 I: a' T" MCC 回收。具體的做法是將函數綁定到某個容器,比如:" i, W6 ?" m0 p- J0 B; a
function displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;5 Q  B* P, S: A" z$ x
對於需要 export 的函數,均使用 quoted string 風格。4 M8 o0 c# P- u
  背後的思考
' s; b; @& _( Y' b$ X  根據以上高級模式優化的行為分析可知,CC 附加給開發者的約束主要有:/ j' K9 D# E& R
強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯
+ O4 q% h# q- Y% w, [0 x1 t$ O" W+ h本身。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即通過開發者與 CC 達成一種
$ i9 [  ^* y6 J% X編碼約定而規避掉。
' X. i7 G6 B) d8 J/ o嚴格要求區分面向開發者的代碼和面向機器的代碼。0 G* m% H* _$ p! n
雖然不像 C 等語言會編譯產生目標代碼,但是 CC 在一定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗余代碼去除。這和第一點其實是一脈相承的,同樣要求開發者將關注點轉移到開發本身。; r, x% g' N, [$ X7 B
使用規範化的接口方式。
% s& W+ o: ^- \1 m" [這不僅包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用同樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,但是帶來的好處是明顯的:可讀、可控、一致性。' [2 i# q. E2 p8 u- N
  曾經有讀過 Closure Library 源碼的童鞋評論道:
( Q4 V! M7 G9 Q. x+ t& sGoogle 根本不懂怎麼寫 javascript!代碼裡面各種冗余,並且充滿了 java 的味道!# `& A0 O/ T" s) _$ Z
  當時確實也有這種感覺,比如 Google 把 if(foo) 寫作 if(foo != undefined) 等等。  h0 J# [- |& E
  Javascript 固然充滿了豐富的動態特徵,而且很多特性非常優雅,能夠讓代碼簡潔精悍,或者構造出一些5 B% m8 ^) n4 q/ j3 \
令人驚歎的技巧,但是也會產生一些副作用:  T7 e( F4 N. D# Y( \; k
首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西需要經過一番運算才能得出結論。
9 ?4 E; [4 \5 @. E: e* z+ B" x! g比如 js 中的極晚綁定,再比如標識符運行時重寫。$ C/ G( \% b5 m# f
其次的問題是執行性能。一個比較經典的眾 js 工程師都在使用的技巧就是「模擬函數重載」——8 i6 A7 G: D- G0 a; ?
在函數體內判斷 arguments 的特徵,從而對應給出不同的邏輯。由於缺乏強類型,js 本身不能具備1 G1 }" q$ T4 U' K
真正的重載,但是運行時的判斷在帶來靈活性的同時,必然會多出很多模擬重載的邏輯,降低性能。
7 q: W( v9 u  u7 d/ z9 |  在今年的 D2 大會上,Hedger 童鞋指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵作戰還可以,但是一旦碰到 army(軍隊,比如 Google 團隊這樣的 )就是個悲劇。$ j" Y) {" y# `* ^
  我比較欣賞這個比喻,大團隊要良好地協作,必需遵循一定的規範和限制,優先保證可讀性和一致性,與此同時5 d# s. |7 p* `+ H0 w9 l" a& ?; S
失去的是奇技淫巧、自由靈活。所以採用何種編程風格、理念,需要具體問題具體分析…………
' e1 E* ^2 v( g7 V  至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑒的。) Q" ~, e0 A7 M4 J! k, L
  最後附上 CC 的常用命令選項……選項實在是有夠多……
" s5 J9 m7 v0 P$ C  CC 常用命令選項
* u2 I& @& c0 ~& Z7 K5 i; u–charset VAL 對所有文件定義的編碼格式: x2 V2 f) f: i3 Z: A. h
–compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]0 ]! _! ?" b$ w5 X9 ]- J
設定編譯級別
5 o2 ]$ W4 `% n–debug 開啟 debug 選項  Q* R+ N; W0 V: h0 X! A+ [& p5 M
–define (–D, -D) VAL 設定文件中使用 @define 標注的開關值,即條件編譯6 X" H5 y$ T; g) z/ `2 s7 k8 z3 p
–externs VAL 編譯代碼需要調用未編譯的代碼時,使用它
3 G* E$ p; n4 _6 J–formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出
7 f4 v" d6 q4 \$ w1 b–js VAL 輸入文件,多指定多個,將會被合併
$ T$ ^1 l( x$ y( e$ z7 `# }9 s5 `–jsoutputfile VAL 輸出文件,如果不指定的話,直接輸出到 standard output 流9 M: w1 j, R* i. T2 f6 K3 }
–module VAL 定義模塊
, M3 g) s# w) j% x–output_manifest VAL 打印編譯文件清單5 v0 L- z0 k$ y$ {4 j4 e9 l5 B
–print_tree 打印語法分析樹7 K$ d( ^$ s) j$ z, l# j
–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 15:53

By DZ X3.5

小黑屋

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