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

前言

  Google Closure Compiler 是 Google Closure Tools 的一員,在 2009 年末被 Google 釋出,早先,有 玉伯 的 Closure Compiler vs. YUICompressor,主要就壓縮率上進行了對比,另外有 承玉 的 應用 closure compiler 高級模式,對 CC 的高級模式作了些介紹。javascript

  本文將詳細介紹 CC 的高級模式部分,更重要的是,闡述 CC 高級模式背後的思考java

  CC 是真正的編譯器編程

  Closure Compiler 和 YUICompressor 並非同類產品,雖然 CC 和 YC 一樣產出壓縮後的 JS 文件,可是 YC 只作了詞法上的掃描,而 CC 並不僅是一個 compressor 那麼簡單,器如其名,它是一個compiler。框架

  對於一個 compiler,通常地,它須要作到:ide

  1. 檢查源文本中語法、語義、語用上的錯誤;
  2. 根據分析產出物(符號表、語法樹等)產出目標 / 中間代碼;
  3. 優化。

  代碼錯誤通常來自三個方面:函數

  1. 語法(Syntax)性能

    表示構成語言句子的各個記號之間的組合規律。大致上,parser / interpreter 在詞法分析和語法分析階段,產生符號表、語法樹等分析產出物,具體見編譯原理教科書……優化

    語法上的錯誤,如:ui

    doSomething(;) // SyntaxError: Unexpected token ;

    根據語法規則,在非 for 語句中的 ; 意義是分隔符,而分隔符前的 ( 並無配對),所以報錯。this

  2. 語義(Semantics)

    表示各個記號的特定含義(各個記號和記號所表示的對象之間的關係)。compiler 須要根據語義分析產出中間代碼,對於不產生中間代碼的語言如 JS,則在運行時的解釋期間指出錯誤。

    語義上的錯誤,如:

    0 = {}; // ReferenceError: Invalid left-hand side in assignment

    根據賦值運算符 = 的意義,左操做數不能爲字面量,因此雖然這個賦值語句包含了必需的左操做數、運算符、右操做數,仍然出錯。

  3. 語用(Pragmatics)

    表示在各個記號所出現的行爲中,它們的來源、使用和影響。

    語用上的錯誤,如:

    doSomething(); // ReferenceError: doSomething is not defined

    在這裏直接調用了一個未定義的函數,致使出錯。在一些其餘場景中,雖然程序運行正確無誤,可是仍然能夠優化(這種優化並非技巧上的),好比:

function doSomethingElse() {}(function() { return; doSomethingElse(); // No Exception but Redundant: Unreachable code})();

  在這裏,doSomethingElse 函數以前因爲有 return,所以這個函數調用將永遠不能執行,這種冗餘代碼對整個程序來講毫無用處,能夠去掉。

  對於 Closure Compiler 來講,它處理的對象是 js,不須要產生其餘中間代碼或彙編代碼 / 機器碼,所以輸出的仍是 js,可是是通過分析的、優化後的 js;另外,它也能夠選擇輸出 parse tree(使用–print_tree 參數),因此,CC 的確完成了一個編譯器須要實現的功能。

  CC 功能概述

  在詳細討論 CC 的高級模式前,仍是簡明介紹一下功能體系。

  編譯級別

  CC 的 compilation_level 包括三個級別:

  1. WHITESPACE_ONLY

    只刪除空白、註釋。

  2. SIMPLE_OPTIMIZATIONS

    在 WHITESPACE_ONLY 基礎上將局部變量和參數轉成短名稱。

  3. ADVANCED_OPTIMIZATIONS

    更加激進的重命名、移除垃圾代碼、內聯函數。

  能夠看到,SIMPLE_OPTIMIZATIONS 級別的 CC,和 YC 無異,沒作什麼真正的編譯工做,因此說,使用了高級模式的 CC 纔是四肢健全的 CC 。

  約束條件

  使用 CC 有必定約束條件,這影響到咱們的編碼風格:

  1. WHITESPACE_ONLY

    • 不承認 JS 1.5 以上版本的語言特性
    • 不保留註釋
  2. SIMPLE_OPTIMIZATIONS

    • 徹底禁用 with 和 eval
    • 字符串中引用的函數名 / 參數名不會改動(CC 不改動全部字符串)
  3. ADVANCED_OPTIMIZATIONS 模式下的約束放到下文詳述

  註解

  Annotations 也是 CC 的重要組成部分,使用 JSDoc 風格,用以輔助高級模式下的編譯,下文詳述。

  使用 CC 高級模式

  在 CC 下,啓用高級模式的方法是加入參數 --compilation_level ADVANCED_OPTIMIZATION。

  做爲一個 compiler,CC 的高級模式下,額外的優化政策是:

  1. 更激進的重命名,如 obj.property 改成 a.b,將深度太高的命名空間平坦化等;
  2. 移除垃圾代碼,如刪除未被調用的方法定義,警告邏輯死角(return 後的語句等);
  3. 將函數內聯,如 a call b, b call c,a(),那麼直接執行 c()。

  要達到高級模式的預期優化效果,開發者必須對本身作一些約束,由於 js 是弱類型、動態性的。不然
js 的這種靈活將使 compiler 無能爲力。

  整體上,這種約束包括限定某些 js 編碼風格,以及使用相應的 JSDoc 註解。

  如下詳述具體的約束以及代碼的檢查 / 優化效果:

  強類型的模擬

  • @param 和 @type 中定義的類型會在編譯期間獲得檢查,一樣避免了在運行時檢查,提升性能。

  • @const 標記常量,當常量被寫時會報錯。

  • 模擬枚舉,將同類可枚舉常量定義爲一個對象字面量,使用 @enum 標記:

    var STATUS = { LOADING: 3, COMPLETE: 4};

    編譯結果中 STATUS.LOADING 會被直接替換爲 3,其實徹底模擬了 C 等語言中的枚舉。

  • 使用 @constructor 標註函數爲構造器,它僅能被實例化,而不可用做普通方法,甚至是工廠方法,CC 會確保構造器被合法使用,不然報錯。這樣確保開發者沒必要在運行時判斷,構造器函數到底以怎樣的形式被調用。

  • 在表達式中也可使用 @type 來限定類型,這對於 JSON 特別有用,如

    var data = /** @type {UserModel} */({ firstName : 'foo', lastName : 'bar'});

    在這裏 UserModel 是個構造器,也可使用 @typedef 來自定義複雜的數據類型。

  域可見性的模擬

  • 使用 @private 標註私有域,私有域被外部引用會報錯。開發者也能夠按照「國際慣例」給私有域加上_ 前綴或後綴,以提醒本身 / 協做者這是一個私有域,@private 註解用來告訴 CC;這樣,開發者能夠沒必要使用諸如老道的「模塊模式」等技巧來真正地隱藏私有變量,將檢查工做丟給CC,讓開發儘量樸實簡單。

  • 相似有 @protect

  類系統的模擬

  • 使用 @extends 標註繼承關係,繼承體系會被優化。

  • 使用 @interface 標註接口,接口是相似 function ThisIsAInterface(obj) {}的函數體爲空的構造器定義,編譯後將移除其相關代碼。同時,標註 @implements 的構造器必須實現implemented 的接口的全部方法(正如其餘 OO 語言同樣),不然,CC 報錯。這一樣簡化了接口 / 實現的約束,靠 CC 來保證明現關係的可靠性。

  條件編譯的模擬

  • 使用 @define 標記狀態開關,適用於調試 logger 等 開發 / 發佈 狀態須要分離的模式。
    能夠在編譯時指定參數來標識 define 參數的狀態。這其實就是一個條件編譯,真給力……

  對象平坦化及屬性名縮減

  • 對象屬性會被編譯爲單變量,好比 foo.bar to foo$bar,這種標記方法看起來很像 java 中被編譯出來的內部類~~以後 foo$bar 被進一步縮短。對象之因此能被平坦化是由於在 js 中對象能夠看作是一羣引用 / 原始數據類型的容器。

  • 可是,js 對象實際上更復雜,因此被平坦化後會帶來一些反作用,好比若是在對象(字面量)中使用 this 指針,則編譯後的結果會致使 this 指向錯誤。因此 Google 建議僅在 constructor 和 prototype methods 中使用 this,這意味着,在所謂類單例(對象字面量)和類的靜態方法(綁定到constructor 上的函數)中都避免使用 this 指針。

  • 在縮減對象屬性 / 方法的名稱長度時,有另一個注意點,那就是必須始終使用 dot syntax(.運算符),而不使用 quoted string([] 運算符),除非索引名是一個變量。這是由於 CC 始終不處理字符串中的內容,因此,var o = { longName: 0 }; o["longName"] 會被翻譯爲var a = { b: 0 }; a["longName"] 致使出錯。實在想使用 quoted string,則在定義的時候也要使用 quoted string。

  • 對於全局變量,若是出現以 window.property 的形式引用的,必須始終定義爲 window.porperty 形式:

    window.property = 1;var property = 1; // wrong!

    不然也會杯具,CC 可不會 window.property 翻譯爲 window.a。

  垃圾代碼的移除

  • 一個函數聲明卻未被調用時,默認地,聲明體將被幹掉。

  • 在這種機制下,若是一個方法是以 for in 的形式調用的,那麼原方法也會被幹掉,由於這種動態特徵使得 CC 沒法清楚方法是否確實在 for in 的時候被調用了。

  • 對於一些 unreachable 的代碼,CC 將報警告。

  • 若是要產出一份被調用的公共接口,例如庫,使用稱做 export 的方法將函數導出,防止函數定義被
    CC 回收。具體的作法是將函數綁定到某個容器,好比:

    function displayNoteTitle(note) { alert(note['myTitle']);}// Store the function in a global property referenced by a string:window['displayNoteTitle'] = displayNoteTitle;

    對於須要 export 的函數,均使用 quoted string 風格。

  背後的思考

  根據以上高級模式優化的行爲分析可知,CC 附加給開發者的約束主要有:

  1. 強制以強類型的靜態語言風格編寫 js,將關注點從運行時的動態技巧轉移到組織代碼、編寫邏輯
    自己。而可能由弱類型系統和動態特徵產生的問題和風險則交給 CC,即經過開發者與 CC 達成一種

    編碼約定而規避掉。

  2. 嚴格要求區分面向開發者的代碼面向機器的代碼

    雖然不像 C 等語言會編譯產生目標代碼,可是 CC 在必定程度上也生成了面向機器的 js,包括壓縮空白、縮減標識符、條件編譯和冗餘代碼去除。這和第一點實際上是一脈相承的,一樣要求開發者將關注點轉移到開發自己。

  3. 使用規範化的接口方式。

    這不只包括要求開發者使用恰當的 annotation(extend, interface, …),同時也給整個 OO-JS 打下了一個框架,開發者必須使用一樣的模式進行 OO 編碼。另外,要求使用 export 技術統一導出公共接口更強化了這一點。總之,這一點進一步限定了開發者的編碼風格,可是帶來的好處是明顯的:可讀、可控、一致性。

  曾經有讀過 Closure Library 源碼的同窗評論道:

Google 根本不懂怎麼寫 javascript!代碼裏面各類冗餘,而且充滿了 java 的味道!

  當時確實也有這種感受,好比 Google 把 if(foo) 寫做 if(foo != undefined) 等等。

  Javascript 當然充滿了豐富的動態特徵,並且不少特性很是優雅,可以讓代碼簡潔精悍,或者構造出一些
使人驚歎的技巧,可是也會產生一些反作用:

  • 首要的問題是可讀性,靜態的東西容易一目瞭然,動態的東西須要通過一番運算才能得出結論。
    好比 js 中的極晚綁定,再好比標識符運行時重寫。

  • 其次的問題是執行性能。一個比較經典的衆 js 工程師都在使用的技巧就是「模擬函數重載」——
    在函數體內判斷 arguments 的特徵,從而對應給出不一樣的邏輯。因爲缺少強類型,js 自己不能具有
    真正的重載,可是運行時的判斷在帶來靈活性的同時,必然會多出不少模擬重載的邏輯,下降性能。

  在今年的 D2 大會上,Hedger 同窗指出,大多數 js 開發者像是個 ninja(忍者),他們身懷絕技、神鬼莫測,單兵做戰還能夠,可是一旦碰到 army(軍隊,好比 Google 團隊這樣的 )就是個悲劇。

  我比較欣賞這個比喻,大團隊要良好地協做,必需遵循必定的規範和限制,優先保證可讀性和一致性,與此同時
失去的是奇技淫巧、自由靈活。因此採用何種編程風格、理念,須要具體問題具體分析…………

  至少,目前 CC 提供了一個好的思路,它的高級模式推崇的編程風格也是很值得嘗試、借鑑的。

  最後附上 CC 的經常使用命令選項……選項實在是有夠多……

  CC 經常使用命令選項

  • –charset VAL 對全部文件定義的編碼格式
  • –compilationlevel [WHITESPACEONLY | SIMPLEOPTIMIZATIONS | ADVANCEDOPTIMIZATIONS]
    設定編譯級別
  • –debug 開啓 debug 選項
  • –define (–D, -D) VAL 設定文件中使用 @define 標註的開關值,即條件編譯
  • –externs VAL 編譯代碼須要調用未編譯的代碼時,使用它
  • –formatting [PRETTYPRINT | PRINTINPUT_DELIMITER] 格式化輸出
  • –js VAL 輸入文件,多指定多個,將會被合併
  • –jsoutputfile VAL 輸出文件,若是不指定的話,直接輸出到 standard output 流
  • –module VAL 定義模塊
  • –output_manifest VAL 打印編譯文件清單
  • –print_tree 打印語法分析樹
  • –warning_level [QUIET | DEFAULT | VERBOSE] 設定報錯模式
相關文章
相關標籤/搜索