[譯]用 Closure Compiler 編寫更好的 OO 的 JavaScript

來源:互聯網
上載者:User
文章目錄
  • 前面的話
  • OO 風格的 JavaScript 決不簡易
  • JavaScript 沒有類
  • JavaScript沒有存取控制
  • 全域變數是糟粕,過深命名空間(deeply name-spaced)的變數也垃圾得很
  • 開發人員不要弄得冗餘
  • 簡化不是簡單
  • 只有 JavaScript 忍者能活嗎?
  • 我們能有所有的精華嗎
  • 讓 Closure Compiler 使你變身為 JavaScript 武將
  • 用 @constructor 來標註函數作為類
  • 用 @private 進行存取控制
  • 用 @extend 來管理類繼承
  • 使用 @interface 和 @implements
  • 使用包(命名空間的 JS 對象)
  • 進行類型檢查是在構建時,而非運行時
  • 使用 @enum
  • 使用 @define 啟用或禁用訊息的記錄
  • 使用類型轉換
  • 還有...
  • 總結

原貼:Coding Better Object-Oriented JavaScript with Closure Compiler

作者:Hedger Wang

前面的話

許多程式員覺得OO 的 JavaScript 是種不錯的方法,但也明白由於語言自身的本質和它所啟動並執行環境(主要是在 網頁瀏覽器中),編寫 OO 風格的 JavaScript 是比較痛苦的。

使用 Google Closure Compiler 不僅可以壓縮代碼,而且可以像別的編譯器那樣編譯代碼!

當編譯器的標幟“ADVANCED_OPTIMIZATIONS(進階選項)” 被貼簽時,與大多的 JavaScript 壓縮公用程式如 YUI Compressor、Dojo Compressor 相比,它擁有更多的最佳化。

我會講幾種通用的 OO 風格的模式,以及是怎樣實現的。

通過這篇文章,你將學會使用 Closure Compiler 來編寫具有 OO 風格的JS代碼。

OO 風格的 JavaScript 決不簡易

長期以來,由於 JavaScript 的本質與瀏覽器這個宿主環境,造成了普通認為:編寫純 OO 的 JavaScript 應用太難了。Nicholas Zakas 是最有名的黑腰帶級空手道 JavaScript 程式員之一,曾在 blog 的貼文中寫過 OO 風格的 JS 代碼所帶來的痛苦:

在傳統的OO語言中,類、繼承是信手拈來。它們的編譯器理解其中的工作原理,所以不會因為加幾個類或多幾個繼承而在執行時受任何的影響。而在 JavaScript 中,參考型別、繼承是2大要命的傷痛處。

JavaScript 沒有類

雖然 OO 風格的JS 在運行時有執行效能上的缺陷,人們還是在嘗試著用各種不同的OO風格的JS,以換掉函數化的JS。

John Resig用了一種有趣的方式以確保在“類”函數作為建構函式時用不用 new 關鍵字都無所謂。

// John Resig 的方法function User(name){  if ( !(this instanceof User) )    return new User(name);  this.name = name;}var userA = new User('John'); // 是一個執行個體var userB = User('Jane'); // 也是一個執行個體
JavaScript沒有存取控制

Douglas Crockford(老道)提出了一種模組模式(module pattern)來說明如何來保護與對象的外部進行讀或寫的私人成員。

// Crockford 的方法function User(name) {  var _name = name;  // 私人 name 的 getter  this.getName = function() {    return _name;  };}
全域變數是糟粕,過深命名空間(deeply name-spaced)的變數也垃圾得很

全域變數是魔鬼,而用過深命名空間的變數也超慢。

// 全域是魔鬼var myProjectDemoUserName = 'foo';// 撒旦...my.project.demo.user.Name = 'foo';
開發人員不要弄得冗餘

在文檔中載入了整個 YUI DOM 庫,卻只用到了其中的一個靜態方法YAHOO.util.Dom.getStyle(document.body, ‘backgroundColor’)時怎樣呢?

簡化不是簡單

也可以在寫 jQuery 庫的外掛程式,但很快就會發現,不能去處理 DOM 範圍、選取器模式,也沒有資料集合、組件架構、類式繼承,沒有任何構建複雜 Web 應用程式開發所需要的東東。

只有 JavaScript 忍者能活嗎?

所以,我們只好去招聘那些請不動的 JavaScript 忍者或是成為其中之一。

那麼,JavaScript 代碼非得寫得這樣嗎:

  if (!("a" in window)) {    var a = 1;  }  alert(a);

其實這是 JavaScript 的糟粕,代碼不能寫成這種鬼樣。比如說 C++、Java 程式員就不會這樣來編寫代碼。程式員的重心應該是在演算法和資料結構,而不是這些旁門左道的技巧。

我們能有所有的精華嗎

看來幾乎不像是在編寫輕量的、通用的、強壯的 JavaScript 來提供功能豐富的、可維護的能力,並且用在很多地方。

想要讓 JavaScript 不再是玩具語言嗎?

那就使用 Closure Compiler 吧。

讓 Closure Compiler 使你變身為 JavaScript 武將

Closure Compiler 是可以使 JavaScript 下載、運行更快的工具。它是真正的 JavaScript 編譯器。將原始碼編譯為機器代碼的替換,將 JavaScript 編譯為更優的 JavaScript。會解析並分析你的 JavaScript,移除無作用程式碼、重寫並最小化。也可以檢查文法、變數引用、類型,並警告 JavaScript 常見的相關陷阱。

在官網上已經有了很多不錯的資源來告訴你如何使用編譯器。寫這篇文章,其實是為了寫一本書中的某章節。讓我們先來解決之前說到的問題。

用 @constructor 來標註函數作為類

想要在運行時會檢查建構函式,可以使用編譯器來為你服務。而且不用在運行時去檢查,要記住的這個核心理念是:在編譯器完成時就可以了。

/** * @constructor */function MyClass() {}// Pass.var obj1 = new MyClass();// ERROR: Constructor function (this:MyClass):// class should be called with the "new" keyword.var obj2 = MyClass(); // Error.
用 @private 進行存取控制
// File demo1.js ///** * A User. * @constructor */function User() {  /**   * The creation date.   * @private   * @type {Date}   */  this._birthDay = new Date();}/** * @return {number} The creation year. */User.prototype.getBirthYear = function() {  return this._birthDay.getYear();};// File demo2.js //// Create a user.var me = new User();// Print out its birth year.document.write(me.getBirthYear().toString());

編譯器會確保私人成員 _birthDay 在整個應用程式的外部不會讀或寫。只在相同的標為 @private的 JS 代碼中才可以訪問到對象。當然,也可以用 @protected 在代碼裡標註。

用 @extend 來管理類繼承

假設我們有3個類:ShapeBoxCube

Shape 類定義了一個通用的方法:getSize()

Box 類繼承 Shape 類。

Cube 類繼承Box

/** * Helper function that implements (pseudo)Classical inheritance inheritance. * @see http://www.yuiblog.com/blog/2010/01/06/inheritance-patterns-in-yui-3/ * @param {Function} childClass * @param {Function} parentClass */function inherits(childClass, parentClass) { /** @constructor */ var tempClass = function() { }; tempClass.prototype = parentClass.prototype; childClass.prototype = new tempClass(); childClass.prototype.constructor = childClass;}///////////////////////////////////////////////////////////////////////////////** * The shape * @constructor */function Shape() { // No implementation.}/** * Get the size * @return {number} The size. */Shape.prototype.getSize = function() { // No implementation.};///////////////////////////////////////////////////////////////////////////////** * The Box. * @param {number} width The width. * @param {number} height The height. * @constructor * @extends {Shape} */function Box(width, height) { Shape.call(this); /** * @private * @type {number} */ this.width_ = width; /** * @private * @type {number} */ this.height_ = height;}inherits(Box, Shape);/** * @return {number} The width. */Box.prototype.getWidth = function() { return this.width_;};/** * @return {number} The height. */Box.prototype.getHeight = function() { return this.height_;};/** @inheritDoc */Box.prototype.getSize = function() { return this.height_ * this.width_;};/////////////////////////////////////////////////////////////////////////////** * The Box. * @param {number} width The width. * @param {number} height The height. * @param {number} depth The depth. * @constructor * @extends {Box} */function Cube(width, height, depth) { Box.call(this, width, height); /** * @private * @type {number} */ this.depth_ = depth;}inherits(Cube, Box);/** * @return {number} The width. */Cube.prototype.getDepth = function() { return this.depth_;};/** @inheritDoc */Cube.prototype.getSize = function() { return this.depth_ * this.getHeight() * this.getWidth();};////////////////////////////////////////////////////////////////////////////var cube = new Cube(3, 6, 9);document.write(cube.getSize().toString());

上面的 JavaScript 代碼有些長,但是原始碼的大小會被簡單的看作是輸入的字元數。

那些文檔中描述編碼的注釋、變數的名稱、方法的名稱會被編譯器重新命名或移除掉。

3層級的類繼承樹會被看作是簡單的函數,編譯器會進行最佳化。

下面是編譯後的代碼:

function d(a, b) { function c() { } c.prototype = b.prototype; a.prototype = new c}function e() {}e.prototype.a = function() {};function f(a, b) { this.c = a; this.b = b}d(f, e);f.prototype.a = function() { return this.b * this.c};function g(a, b, c) { f.call(this, a, b); this.d = c}d(g, f);g.prototype.a = function() { return this.d * this.b * this.c};document.write((new g(3, 6, 9)).a().toString());

雖然所有的變數和方法都改名了,但是你也注意到:有些方法被移除掉,有些方法合成了一行內。比如:

Cube.prototype.getSize = function() { return this.depth_ * this.getHeight() * this.getWidth();};

變成了:

g.prototype.a = function() { return this.d * this.b * this.c};

顯然,2個 getter 方法 getWidth()getHeight() this._widththis._height安全的替換掉。因此,那些 getter 已經沒有用,並且被編譯器移除掉了。

同時使用了 @private 和 getter 的方法是指私人屬性 _width 對開發人員來說是唯讀,無妨對其添加一個 getter 方法。

使用 @interface 和 @implements

我們對編寫 OO 風格的 JavaScript 感興趣了後,將上面的樣本改成下面的代碼。

// skip example code./////////////////////////////////////////////////////////////////////////////** * The shape * @interface */function Shape() {}/** * Get the size * @return {number} The size. */Shape.prototype.getSize = function() {};/////////////////////////////////////////////////////////////////////////////** * The Box. * @param {number} width The width. * @param {number} height The height. * @constructor * @implements {Shape} */function Box(width, height) {  Shape.call(this);  /**   * @private   * @type {number}   */  this.width_ = width;  /**   * @private   * @type {number}   */  this.height_ = height;}/** * @return {number} The width. */Box.prototype.getWidth = function() {  return this.width_;};// skip example code.

由於 @interface 只用在編譯的時候,經過編譯後的代碼更小了,並且不會輸出包含介面的代碼。

function d(a, b) {  this.c = a;  this.b = b}d.prototype.a = function() {  return this.b * this.c};function e(a, b, c) {  d.call(this, a, b);  this.d = c}(function(a, b) {  function c() {  }  c.prototype = b.prototype;  a.prototype = new c})(e, d);e.prototype.a = function() {  return this.d * this.b * this.c};document.write((new e(3, 6, 9)).a().toString());
使用包(命名空間的 JS 對象)

想要對 JS 對象使用命名空間的話,命名層級過深的問題不會影響運行時的效能,因為編譯器會幫你解決掉。

// Create namespaces.var demo = {};demo.example = {};demo.example.exercise = {};/** * @constructor */demo.example.exercise.Foo = function() {  demo.example.exercise.Foo.print(this.value1);  demo.example.exercise.Foo.print(this.value2);};/** * Static method * @param {string} str String to print. */demo.example.exercise.Foo.print = function(str) {  document.write(str);};/** * @type {string} */demo.example.exercise.Foo.prototype.value1 = 'abc';/** * @type {string} */demo.example.exercise.Foo.prototype.value2 = 'def';var foo = new demo.example.exercise.Foo();

編譯後的代碼:

function a() {  document.write(this.a);  document.write(this.b)}a.prototype.a = "abc";a.prototype.b = "def";new a;

也許,想要保留 JS 代碼而避免與頁面中其他指令碼產生衝突的話,可以使用標幟 -output_wrapper,也不是全域的對象(除非是明確的匯出)。

編譯後的代碼如下:

(function() {function a() {  document.write(this.a);  document.write(this.b)}a.prototype.a = "abc";a.prototype.b = "def";new a;})()

編譯器會確保那些長的命名空間、屬性、方法已經重新命名,儘可能多的簡短名稱。

進行類型檢查是在構建時,而非運行時

在構建時進行類型檢查可以減少不必要的在運行時進行的類型檢查。比如:

function User() {}function UsersGroup() {  this.users_ = [];}UsersGroup.prototype.add = function(user) {  // Make sure that only user can be added.  if (!(user instanceof User)) {    throw new Error('Only user can be added.');  }  this.users_.push(user);};var me = new User();var myGroup = new UsersGroup();myGroup.add(me);

這種方法可以完成。

/** * @constructor */function User() {}/** * @constructor */function UsersGroup() {  /**   * @private   * @type {Array.<User>}   */  this.users_ = [];}/*** @param {User} user*/UsersGroup.prototype.add = function(user) {  this.users_.push(user);};

注意 this.users_ 的資料類型為 @type {Array.<user>} 表示是 User 的一個列表。

應該使用有意義的資料結構,而不是視任何事物為原生的對象,否則非常容易出錯。

使用 @enum

有些時候你想要處理多種情形:

function Project(status) {  this.status_ = status;}Project.prototype.isBusy = function() {  switch (this.status_) {    case 'busy':;    case 'super_busy':      return true;    default:      return false;  }};var p1 = new Project('busy');var p2 = new Project('super_busy');var p3 = new Project('idle');document.write(p1.isBusy().toString());document.write(p2.isBusy().toString());document.write(p3.isBusy().toString());

可以考慮使用 @enum

/** * @constructor * @param {Project.Status} status */function Project(status) {  /**   * @type {Project.Status}   * @private   */  this.status_ = status;}/** * @enum {number} */Project.Status = {  BUSY: 0,  SUPER_BUSY: 1,  IDLE: 2};/** * @return {boolean} */Project.prototype.isBusy = function() {  switch (this.status_) {    case Project.Status.BUSY:;    case Project.Status.SUPER_BUSY:      return true;    default:      return false;  }};var p1 = new Project(Project.Status.BUSY);var p2 = new Project(Project.Status.SUPER_BUSY);var p3 = new Project(Project.Status.IDLE);document.write(p1.isBusy().toString());document.write(p2.isBusy().toString());document.write(p3.isBusy().toString());

編譯後為:

function a(b) {  this.a = b}function c(b) {  switch(b.a) {    case 0:    ;    case 1:      return true;    default:      return false  }}var d = new a(1), e = new a(2);document.write(c(new a(0)).toString());document.write(c(d).toString());document.write(c(e).toString());

枚舉變數被原始的數替換。使用枚舉能夠編寫更多的可維護性的代碼。

使用 @define 啟用或禁用訊息的記錄

如果你想要在一個類中記錄某些重要的訊息的話,像每一位謹慎的程式員那樣都可以做到。

/** * namespace for the Logger. */var Logger = {};/** * Whether logging should be enabled. * @define {boolean} */Logger.ENABLED = true;/** * the log API. * @param {...*} args */Logger.log = function(args) {  if (!Logger.ENABLED) {    // Don't do anything if logger is disabled.    return;  }  var console = window['console'];  if (console) {    console['log'].apply(console, arguments);  }};/** * A User. * @param {string} name * @constructor */function User(name) {  Logger.log('New User', name);}var me = new User('me');

代碼會編譯為:

function b() {  var a = window.console;  a && a.log.apply(a, arguments)}new function(a) {  b("New User", a)}("me");

你可以添加標幟 –define Logger.ENABLED=false 來禁用記錄器。也可以添加標幟 –jscomp_error unknownDefines 來捕獲未知的 @define

java -jar compiler.jar \  --js src/demo.js \  --js_output_file compiled/demo.js \  --warning_level VERBOSE \  --formatting PRETTY_PRINT \  --jscomp_error accessControls \  --jscomp_error checkTypes \  --jscomp_error unknownDefines  --define Logger.ENABLED=false  --compilation_level ADVANCED_OPTIMIZATIONS;

對開發人員來說,允許在產生代碼時啟用記錄器,或是完全由編譯器帶所有的記錄器調用到生產布署。

使用類型轉換

有些時候你想要把 JSON 對象轉換為未知的參考型別。比如:

/** * The Model definition. * @constructor */function UserModel() {  /**   * @type {string}   */  this.firstName = '';  /**   * @type {string}   */  this.lastName = '';}//////////////////////////////////////////////////////////////////////////////** * The User constructor. * @constructor * @param {string} firstName * @param {string} lastName */function User(firstName, lastName) {  /**   * @type {string}   */  this.fullName = firstName + ' ' + lastName;}/** * A static method that creates a User from a model. * @param {UserModel} model * @return {User} The user created. */User.createFromUserModel = function(model) {  return new User(model.firstName, model.lastName);};/////////////////////////////////////////////////////////////////////////////// Cast a simple JSON Object as {UserModel}.var data = /** @type {UserModel} */({  firstName : 'foo',  lastName : 'bar'});// Create a user from the model.var user = User.createFromUserModel(data);document.write(user.fullName);

正如你的意料之中,model definition 會移除掉,屬性 firstNamelastName 也會重新命名。

var a = {a:"foo", c:"bar"};document.write((new function(b, c) {  this.b = b + " " + c}(a.a, a.c)).b);

在上面的樣本中,純對象轉換為未知的參考型別,可以給該對象更詳細的指定。

在 jQuery 1.4 中添加了新的 API isPlainObject 它是在運行時進行類型檢查,而我將會不推薦,如果你有編譯器在手的話,其實在 JS 中看來似乎是解決一大難題。

還有...

還有很多其他使用的東西,比如對常量使用 @const

在此本人推薦 Closure 工具官網去學習更多的知識。

另外,有一本不錯的書《Closure 權威指南》(打個廣告)有所有 closure 工具的詳細內容。

總結

本人已經使用 Google Closure Compiler 有兩年多了,它完全改變了 JavaScript 開發方式。

總之,在此有以下的東西要分享:

  1. 想要堅持不錯的、統一的代碼、風格(比如縮排)、80或120字元寬度限制等等,請參考 Google JavaScript Style Guide(中文)。

    請確保代碼的可讀性和可維護性。

  2. 編寫兼具描述性與資訊性的文檔,有些時候需要編寫更多的 JsDoc。
  3. 把時間和精力更多的放在不錯的 OO 設計、演算法、資料結構上,而不是浪費在細微的最佳化代碼或是使用任何的忍者技,那樣會不易讀或搞昏。
  4. 編寫代碼快速而頻繁,並且產生代碼。
  5. 使用大型 JavaScript 庫沒有錯誤,只要你可以用編譯器來產生代碼。其實能夠得到更少的代碼。
  6. 在 Closure Compiler 的 ADVANCED_OPTIMIZATIONS 模式中確保代碼的相容。

    確保很多品質更高的代碼,會使產生整合其他現有的編譯器相容的 JS 代碼更加容易。

(完)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.