JavaScript 模塊化編程 - Module Pattern

## 前言

The Module Pattern,模塊模式,也譯爲模組模式,是一種通用的對代碼進行模塊化組織與定義的方式。這裏所說的模塊(Modules),是指實現某特定功能的一組方法和代碼。許多現代語言都定義了代碼的模塊化組織方式,好比 Golang 和 Java,它們都使用 package 與 import 來管理與使用模塊,而目前版本的 JavaScript 並未提供一種原生的、語言級別的模塊化組織模式,而是將模塊化的方法交由開發者來實現。所以,出現了不少種 JavaScript 模塊化的實現方式,好比,CommonJS Modules、AMD 等。 javascript

以 AMD 爲例,該規範使用 define 函數來定義模塊。使用 AMD 規範進行模塊化編程是很簡單的,大體上的結構是這樣的: css

define(factory(){
  // 模塊代碼
  // return something;
});

目前尚在制定中的 Harmony/ECMAScript 6(也稱爲 ES.next),會對模塊做出語言級別的定義,但距離實用尚高不可攀,這裏暫時不討論它。 html

做爲一種模式,模塊模式其實一直伴隨着 JavaScript 存在,與 ES 6 無關。最近我須要重構本身的一些代碼,所以我參考和總結了一些實用的模塊化編程實踐,以便更好的組織個人代碼。須要注意的是,本文只是我的的一個總結,比較簡單和片面,詳盡的內容與剖析請參看文後的參考資料,它們寫得很好。本文並不關心模塊如何載入,只關心現今該如何組織模塊化的代碼。還有,沒必要過於糾結所謂的模式,真正重要的其實仍是模塊代碼及思想。所謂模式,不過是咱們書寫代碼的一些技巧和經驗的總結,是一些慣用法,實踐中應靈活運用。 java

## 模塊模式

### 閉包與 IIFE (Immediately-Invoked Function Expression)

模塊模式使用了 JavaScript 的一個特性,即閉包(Closures)。現今流行的一些 JS 庫中常常見到如下形式的代碼: jquery

;(function (參數) {
  // 模塊代碼
  // return something;
})(參數);

上面的代碼定義了一個匿名函數,並當即調用本身,這叫作自調用匿名函數(SIAF),更準確一點,稱爲當即調用的函數表達 (Immediately-Invoked Function Expression, IIFE–讀作「iffy」)。 git

在閉包中,能夠定義私有變量和函數,外部沒法訪問它們,從而作到了私有成員的隱藏和隔離。而經過返回對象或函數,或是將某對象做爲參數傳入,在函數體內對該對象進行操做,就能夠公開咱們所但願對外暴露的公開的方法與數據。 github

這,其實就是模塊模式的本質。 web

注1:上面的代碼中,最後的一對括號是對匿名函數的調用,所以必不可少。而前面的一對圍繞着函數表達式的一對括號並非必需的,但它能夠用來給開發人員一個指示 -- 這是一個 IIFE。也有一些開發者在函數表達式前面加上一個驚歎號(!)或分號(;),而不是用括號包起來。好比 knockoutjs 的源碼大體就是這樣的: ajax

!function (參數) {
  // 代碼
  // return something
}(參數);

還有些人喜歡用括號將整個 IIFE 圍起來,這樣就變成了如下的形式:express

(function (參數) {
  // 代碼
  // return something
}(參數));

注2:在有些人的代碼中,將 undefined 做爲上面代碼中的一個參數,他們那樣作是由於 undefined 並非 JavaScript 的保留字,用戶也能夠定義它,這樣,當判斷某個值是不是 undefined 的時候,判斷可能會是錯誤的。將 undefined 做爲一個參數傳入,是但願代碼能按預期那樣運行。不過我認爲,通常狀況下那樣作並沒太大意義。

### 參數輸入

JavaScript 有一個特性叫作隱式全局變量(implied globals),當使用一個變量名時,JavaScript 解釋器將反向遍歷做用域鏈來查找變量的聲明,若是沒有找到,就假定該變量是全局變量。這種特性使得咱們能夠在閉包裏隨處引用全局變量,好比 jQuery 或 window。然而,這是一種很差的方式。

考慮模塊的獨立性和封裝,對其它對象的引用應該經過參數來引入。若是模塊內須要使用其它全局對象,應該將這些對象做爲參數來顯式引用它們,而非在模塊內直接引用這些對象的名字。以 jQuery 爲例,若在參數中沒有輸入 jQuery 對象就在模塊內直接引用 $ 這個對象,是有出錯的可能的。正確的方式大體應該是這樣的:

;(function (q, w) {
  // q is jQuery
  // w is window
  // 局部變量及代碼
  // 返回
})(jQuery, window);

相比隱式全局變量,將引用的對象做爲參數,使它們得以和函數內的其它局部變量區分開來。這樣作還有個好處,咱們能夠給那些全局對象起一個別名,好比上例中的 "q"。如今看看你的代碼,是否沒有通過對 jQuery 的引用就處處都是"$"?

### 模塊輸出(Module Export)

有時咱們不僅是要使用全局變量,咱們也要聲明和輸出模塊中的對象,這能夠經過匿名函數的 return 語句來達成,而這也構成了一個完整的模塊模式。來看一個完整的例子:

var MODULE = (function () {
	var my = {},
		privateVariable = 1;

	function privateMethod() {
		// ...
	}

	my.moduleProperty = 1;
	my.moduleMethod = function () {
		// ...
	};

	return my;
}());

這段代碼聲明瞭一個變量 MODULE,它帶有兩個可訪問的屬性:moduleProperty 和 moduleMethod,其它的代碼都封裝在閉包中保持着私有狀態。參考之前提過的參數輸入,咱們還能夠經過參數引用其它全局變量。

#### 輸出簡單對象

不少時候咱們 return 一個對象做爲模塊的輸出,好比上例就是。

另外,使用對象直接量(Object Literal Notation)來表達 JavaScript 對象是很常見的。好比:var x = { p1: 1, p2: "2", f: function(){ /*... */ } }

不少時候咱們都能見到這樣的模塊化代碼:

var Module1 = (function () {
  var private_variable = 1;
  function private_method() { /*...*/ }

  var my = {
    property1: 1,
    property2: private_variable,
    method1: private_method,
    method2: function () {
        // ...
    }
  };
  return my;
}());

另外,對於簡單的模塊化代碼,若不涉及私有成員等,其實也能夠直接使用對象直接量來表達一個模塊:

var Widget1 = {
  name: "who am i?",
  settings: {
    x: 0,
    y: 0
  },
  call_me: function () {
    // ...
  }
};

有一篇文章講解了這種形式: How Do You Structure JavaScript? The Module Pattern Edition

不過這只是一種簡單的形式,你能夠將它看做是模塊模式的一種基礎的簡單表達形式,而把閉包形式看做是對它的一個封裝。

#### 輸出函數

有時候咱們但願返回的並非一個對象,而是一個函數。有兩種需求要求咱們返回一個函數,一種狀況是咱們須要它是一個函數,好比 jQuery,它是一個函數而不是一個簡單對象;另外一種狀況是咱們須要的是一個「類」而不是一個直接量,以後咱們能夠用 "new" 來實例它。目前版本的 JavaScript 並無專門的「類」定義,但它卻能夠經過 function 來表達。

var Cat = (function () {
  // 私有成員及代碼 ...

  return function(name) {
    this.name = name;
    this.bark = function() { /*...*/ }
  };
}());

var tomcat = new Cat("Tom");
tomcat.bark();

爲何不直接定義一個 function 而要把它放在閉包裏呢?簡單點的狀況,確實不須要使用 IIFE 這種形式,但複雜點的狀況,在構造咱們所須要的函數或是「類」時,若須要定義一些私有的函數,就有必要使用 IIFE 這種形式了。

另外,在 ECMAScript 第五版中,提出了 Object.create() 方法。這時能夠將一個對象視做「類」,並使用 Object.create() 進行實例化,不需使用 "new"。

### Revealing Module Pattern

前面已經提到一種形式是輸出對象直接量(Object Literal Notation),而 Revealing Module Pattern 其實就是這種形式,只是作了一些限定。這種模式要求在私有範圍內中定義變量和函數,而後返回一個匿名對象,在該對象中指定要公開的成員。參見下面的代碼:

var MODULE = (function () {
  // 私有變量及函數
  var x = 1;
  function f1() {}
  function f2() {}

  return {
    public_method1: f1,
    public_method2: f2
  };
}());

## 模塊模式的變化

### 擴展

上面的舉例都是在一個地方定義模塊,若是咱們須要在數個文件中分別編寫一個模塊的不一樣部分該怎麼辦呢?或者說,若是咱們須要對已有的模塊做出擴展該怎麼辦呢?其實也很簡單,將模塊對象做爲參數輸入,擴展後再返回本身就能夠了。好比:

var MODULE = (function (my) {
  my.anotherMethod = function () {
    // added method...
  };

  return my;
}(MODULE));

上面的代碼爲對象 MODULE 增長了一個 "anotherMethod" 方法。

### 鬆耦合擴展(Loose Augmentation)

上面的代碼要求 MODULE 對象是已經定義過的。若是這個模塊的各個組成部分並無加載順序要求的話,其實能夠容許輸入的參數爲空對象,那麼咱們將上例中的參數由 MODULE 改成 MODULE || {} 就能夠了:

var MODULE = (function (my) {
  // add capabilities...
  return my;
}(MODULE || {}));


### 緊耦合擴展(Tight Augmentation)

與上例不一樣,有時咱們要求在擴展時調用之前已被定義的方法,這也有可能被用於覆蓋已有的方法。這時,對模塊的定義順序是有要求的。

var MODULE = (function (my) {
  var old_moduleMethod = my.moduleMethod;

  my.moduleMethod = function () {
    // 方法重載
    // 可經過 old_moduleMethod 調用之前的方法...
  };

  return my;
}(MODULE));


### 克隆與繼承(Cloning and Inheritance)


var MODULE_TWO = (function (old) {
	var my = {},
		key;

	for (key in old) {
		if (old.hasOwnProperty(key)) {
			my[key] = old[key];
		}
	}

	var super_moduleMethod = old.moduleMethod;
	my.moduleMethod = function () {
		// override method on the clone, access to super through super_moduleMethod
	};

	return my;
}(MODULE));

有時咱們須要複製和繼承原對象,上面的代碼演示了這種操做,但未必完美。若是你可使用 Object.create() 的話,請使用 Object.create() 來改寫上面的代碼:

var MODULE_TWO = (function (old) {
  var my = Object.create(old);

  var super_moduleMethod = old.moduleMethod;
  my.moduleMethod = function () {
    // override method ...
  };

  return my;
}(MODULE));


### 子模塊(Sub-modules)

模塊對象固然能夠再包含子模塊,形如 MODULE.Sub=(function(){}()) 之類,這裏再也不展開敘述了。

### 各類形式的混合

以上介紹了常見的幾種模塊化形式,實際應用中有多是這些形式的混合體。好比:

var UTIL = (function (parent, $) {
	var my = parent.ajax = parent.ajax || {};

	my.get = function (url, params, callback) {
		// ok, so I'm cheating a bit :)
		return $.getJSON(url, params, callback);
	};

	// etc...

	return parent;
}(UTIL || {}, jQuery));


## 與其它模塊規範或 JS 庫的適配

### 模塊環境探測

現今,CommonJS Modules 與 AMD 有着普遍的應用,若是肯定 AMD 的 define 是可用的,咱們固然可使用 define 來編寫模塊化的代碼。然而,咱們不能假定咱們的代碼必然運行於 AMD 環境下。有沒有辦法可讓咱們的代碼既兼容於 CommonJS Modules 或 AMD 規範,又能在通常環境下運行呢?

其實咱們只須要在某個地方加上對 CommonJS Modules 與 AMD 的探測並根據探測結果來「註冊」本身就能夠了,以上那些模塊模式仍然有用。

AMD 定義了 define 函數,咱們可使用 typeof 探測該函數是否已定義。若要更嚴格一點,能夠繼續判斷 define.amd 是否有定義。另外,SeaJS 也使用了 define 函數,但和 AMD 的 define 又不太同樣。

對於 CommonJS,能夠檢查 exports 或是 module.exports 是否有定義。

如今,我寫一個比較直白的例子來展現這個過程:

var MODULE = (function () {
  var my = {};
  // 代碼 ...

  if (typeof define == 'function') {
    define( function(){ return my; } );
  }else if (typeof module != 'undefined' && module.exports) {
    module.exports = my;
  }
  return my;
}());

上面的代碼在返回 my 對象以前,先檢測本身是不是運行在 AMD 環境之中(檢測 define 函數是否有定義),若是是,就使用 define 來定義模塊,不然,繼續檢測是否運行於 CommonJS 中,好比 NodeJS,若是是,則將 my 賦值給 module.exports。所以,這段代碼應該能夠同時運行於 AMD、CommonJS 以及通常的環境之中。另外,咱們的這種寫法應該也可在 SeaJS 中正確執行。

### 其它一些 JS 庫的作法

如今許多 JS 庫都加入了對 AMD 或 CommonJS Modules 的適應,好比 jQuery, Mustache, doT, Juicer 等。

jQuery 的寫法可參考 exports.js:

if ( typeof module === "object" && module && typeof module.exports === "object" ) {
	module.exports = jQuery;
} else {
	if ( typeof define === "function" && define.amd ) {
		define( "jquery", [], function () { return jQuery; } );
	}
}

if ( typeof window === "object" && typeof window.document === "object" ) {
	window.jQuery = window.$ = jQuery;
}

與前面我寫的那段代碼有些不一樣,在對 AMD 和 CommonJS 探測以後,它將 jQuery 註冊成了 window 對象的成員。

然而,jQuery 是一個瀏覽器端的 JS 庫,它那樣寫固然沒問題。但若是咱們所寫的是一個通用的庫,就不該使用 window 對象了,而應該使用全局對象,而這通常可使用 this 來獲得。

咱們看看 Mustache 是怎麼作的:

(function (root, factory) {
  if (typeof exports === "object" && exports) {
    factory(exports); // CommonJS
  } else {
    var mustache = {};
    factory(mustache);
    if (typeof define === "function" && define.amd) {
      define(mustache); // AMD
    } else {
      root.Mustache = mustache; // <script>
    }
  }
}(this, function (mustache) {
  // 模塊主要的代碼放在這兒
});

這段代碼與前面介紹的方式不太同樣,它使用了兩個匿名函數。後面那個函數能夠看做是模塊代碼的工廠函數,它是模塊的主體部分。前面那個函數對運行環境進行檢測,根據檢測的結果對模塊的工廠函數進行調用。另外,做爲一個通用庫,它並沒使用 window 對象,而是使用了 this,由於在簡單的函數調用中,this 其實就是全局對象。

再看看 doT 的作法。doT 的作法與 Mustache 不一樣,而是更接近於我在前面介紹 AMD 環境探測的那段代碼:

(function() {
	"use strict";

	var doT = {
		version: '1.0.0',
		templateSettings: { /*...*/ },
		template: undefined, //fn, compile template
		compile:  undefined  //fn, for express
	};

	if (typeof module !== 'undefined' && module.exports) {
		module.exports = doT;
	} else if (typeof define === 'function' && define.amd) {
		define(function(){return doT;});
	} else {
		(function(){ return this || (0,eval)('this'); }()).doT = doT;
	}
	// ...
}());

這段代碼裏的 (0, eval)('this') 是一個小技巧,這個表達式用來獲得 Global 對象,'this' 實際上是傳遞給 eval 的參數,但因爲 eval 是經由 (0, eval) 這個表達式間接獲得的,所以 eval 將會在全局對象做用域中查找 this,結果獲得的是全局對象。如果代碼運行於瀏覽器中,那麼獲得的實際上是 window 對象。這裏有一個針對它的討論: http://stackoverflow.com/questions/14119988/return-this-0-evalthis/14120023#14120023

其實也有其它辦法來獲取全局對象的,好比,使用函數的 call 或 apply,但不給參數,或是傳入 null:

var global_object = (function(){ return this; }).call();

你能夠參考這篇文章: Javascript的this用法

Juicer 則沒有檢測 AMD,它使用了以下的語句來檢測 CommonJS Modules:

typeof(module) !== 'undefined' && module.exports ? module.exports = juicer : this.juicer = juicer;

另外,你還能夠參考一下這個: https://gist.github.com/kitcambridge/1251221

(function (root, Library) {
  // The square bracket notation is used to avoid property munging by the Closure Compiler.
  if (typeof define == "function" && typeof define["amd"] == "object" && define["amd"]) {
    // Export for asynchronous module loaders (e.g., RequireJS, `curl.js`).
    define(["exports"], Library);
  } else {
    // Export for CommonJS environments, web browsers, and JavaScript engines.
    Library = Library(typeof exports == "object" && exports || (root["Library"] = {
      "noConflict": (function (original) {
        function noConflict() {
          root["Library"] = original;
          // `noConflict` can't be invoked more than once.
          delete Library.noConflict;
          return Library;
        }
        return noConflict;
      })(root["Library"])
    }));
  }
})(this, function (exports) {
  // ...
  return exports;
});

我以爲這個寫得有些複雜了,我也未必須要個人庫帶有 noConflict 方法。不過,它也能夠是個不錯的參考。

## JavaScript 模塊化的將來

將來的模塊化方案會是什麼樣的?我不知道,但無論未來如何演化,做爲一種模式,模塊模式是不會過期和消失的。

如前所述,尚在制定中的 ES 6 會對模塊做出語言級別的定義。咱們來看一個實例,如下的代碼段摘自「ES6:JavaScript中將會有的幾個新東西」:

module Car {  
  // 內部變量
  var licensePlateNo = '556-343';  
  // 暴露到外部的變量和函數
  export function drive(speed, direction) {  
    console.log('details:', speed, direction);  
  }  
  export module engine{  
    export function check() { }  
  }  
  export var miles = 5000;  
  export var color = 'silver';  
};

我不知道 ES 6 未來會否對此做出改變,對上面的這種代碼形式,不一樣的人會有不一樣的見解。就我我的而言,我十分不喜歡這種形式!

確實,咱們可能須要有一種統一的模塊化定義方式。發明 AMD 和 RequireJS 的人也說過 AMD 和 RequireJS 應該被淘汰了,運行環境應該提供模塊的原生支持。然而,ES 6 中的模塊定義是不是正確的?它是不是一個好的解決方案呢?我不知道,但我我的真的很不喜歡那種方式。不少人十分喜歡把其它語言的一些東西生搬硬套到 JavaScript 中,或是孜孜不倦地要把 JavaScript 變成另一種語言,我至關討厭這種行爲。我並不是一個保守的人,我樂意接受新概念、新語法,只要它是好的。可是,ES 6 草案中的模塊規範是我不喜歡的,起碼,我認爲它脫離了現實,否認了開源社區的實踐和經驗,是一種意淫出來的東西,這使得它在目前不能解決任何實際問題,反而是來添亂的。

按目前的 ES6 草案所給出的模塊化規範,它並無採用既有的 CommonJS Modules 和 AMD 規範,而是定義了一種新的規範,並且這種規範修改了 JavaScript 既有的語法形式,使得它沒有辦法像 ES5 中的 Object.create、Array.forEach 那樣能夠利用現有版本的 JavaScript 編寫一些代碼來實現它。這也使得 ES 6 的模塊化語法將在一段時期內處於不可用的狀態。

引入新的語法也不算是問題,然而,爲了模塊而大費周折引出那麼多新的語法和定義,真的是一種好的選擇麼?話說,它解決了什麼實質性的問題而非如此不可?現今流行的 AMD 其實簡單到只定義了一個 "define" 函數,它有什麼重大問題?就算那些專家因種種緣由或目的而沒法接受 AMD 或其它開源社區的方案,稍做出一些修改和中和老是能夠的吧,非要把 JavaScript 改頭換面不可麼?確實有人寫了一些觀點來解釋爲什麼不用 AMD,然而,那些解釋和觀點其實大都站不住腳。好比說,其中一個解釋是 AMD 規範不兼容於 ES 6!好笑不好笑?ES 6 還沒有正式推出,徹底實現了 ES 6 的 JavaScript 運行時也沒幾個,而 AMD 在開源社區中早已十分流行,這個時候說 AMD 不兼容 ES 6,我不知道這是什麼意思。

就我看來,現今各類形形色色的所謂標準化工做組,不少時候像是高高在上的神仙,他們拉不下臉全身心地參與到開源社區之中,他們就是要做出與開源社區不一樣的規範,以此來彰顯他們的工做、專業與權威。並且,不少時候他們過於官僚,又或者夾雜在各大商業集團之間猶豫不定。我不否定他們工做的重要性,然而,以專家自居而脫離或否認開源社區的實踐,以及商業與政治的利益均衡等,使得他們的工做與開源社區相比,在技術的推進與發展上成效不足甚至添亂。

回到 ES 6 中的模塊,想一想看,我須要修改個人代碼,在其中加上諸如 module, export, import 之類的新的語法,修改以後的代碼卻沒辦法在現今版本的 JavaScript 中運行,並且,與現今流行的模塊化方案相比,這些工做也沒什麼實質性的幫助,想一想這些,我只感受像是吃了一個蒼蠅。

ES 6 的發展固然不會由於個人吐嘈而有任何變化,我也不肯再展開討論。將來的模塊化方案具體是什麼樣的沒法知曉,但起碼我能夠獲得如下的結論:

  • 模塊模式不會過期
  • ES 6 不會接納 AMD 等現有方案,但無論如何,JavaScript 將會有語言級別的模塊定義
  • ES 6 中的模塊在一段時期內是不可用的
  • 即便 ES 6 已達到實用階段,現今的模塊化方案仍會存在和發展

## 參考資料

(完)

版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0

相關文章
相關標籤/搜索