前言:
現在javascript庫特別多,其寫法各式各樣,總結幾種我們經常見到的,作為自己知識的積累。而目前版本的 JavaScript 並未提供一種原生的、語言級別的模塊化組織模式,而是將模塊化的方法交由開發者來實現。因此,出現了很多種 JavaScript 模塊化的實現方式,
以 AMD 為例,該規范使用 define 函數來定義模塊。
define(factory(){ //模塊化 });
模塊模式:
模塊模式使用了 JavaScript 的一個特性,即閉包(Closures)。現今流行的一些 JS 庫中經常見到以下形式的代碼:
;(function (參數) { // 模塊代碼 // return something; })(參數);
上面的代碼定義了一個匿名函數,並立即調用自己。
也有一些開發者在函數表達式前面加上一個驚嘆號(!)或分號(;),而不是用括號包起來。
!function (參數) { // 代碼 // return something }(參數);
還有些人喜歡用括號將整個 IIFE 圍起來,這樣就變成了以下的形式:
(function (參數) { // 代碼 // return something }(參數));
參數輸入:
JavaScript 有一個特性叫做隱式全局變量(implied globals),當使用一個變量名時,JavaScript 解釋器將反向遍歷作用域鏈來查找變量的聲明,如果沒有找到,就假定該變量是全局變量。這種特性使得我們可以在閉包里隨處引用全局變量,比如 jQuery 或 window。然而,這是一種不好的方式。
考慮模塊的獨立性和封裝,對其它對象的引用應該通過參數來引入。如果模塊內需要使用其它全局對象,應該將這些對象作為參數來顯式引用它們,而非在模塊內直 接引用這些對象的名字。以 jQuery 為例,若在參數中沒有輸入 jQuery 對象就在模塊內直接引用 $ 這個對象,是有出錯的可能的。正確的方式大致應該是這樣的:
;(function ($, w) { // $ is jQuery // w is window // 局部變量及代碼 // 返回 })(jQuery, window);
模塊輸出(Module Export)
有時我們不只是要使用全局變量,我們也要聲明和輸出模塊中的對象,這可以通過匿名函數的 return 語句來達成,而這也構成了一個完整的模塊模式。
var klm= (function () { var myklm = {}, modeval= 1; function privateMethod() { // ... } myklm.moduleProperty = 1; myklm.moduleMethod = function () { // ... }; return myklm ; }());
這段代碼聲明了一個變量 MODULE,它帶有兩個可訪問的屬性:moduleProperty 和 moduleMethod,其它的代碼都封裝在閉包中保持着私有狀態。參考以前提過的參數輸入,我們還可以通過參數引用其它全局變量。
輸出簡單對象:
使用對象直接量來表達 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; }());
輸出函數:
有時候我們希望返回的並不是一個對象,而是一個函數。有兩種需求要求我們返回一個函數,一種情況是我們需要它是一個函數,比如 jQuery,它是一個函數而不是一個簡單對象;另一種情況是我們需要的是一個“類”而不是一個直接量,之后我們可以用 "new" 來實例它。目前版本的 JavaScript 並沒有專門的“類”定義,但它卻可以通過 function 來表達。
var klm= (function () { // 私有成員及代碼 ... return function(name) { this.name = name; this.bark = function() { /*...*/ } }; }()); var com = new klm("蒯靈敏"); com.bark();
前面已經提到一種形式是輸出對象直接量(Object Literal Notation),而 Revealing Module Pattern 其實就是這種形式,只是做了一些限定。這種模式要求在私有范圍內中定義變量和函數,然后返回一個匿名對象,在該對象中指定要公開的成員。
var klm= (function () { // 私有變量及函數 var x = 1; function f1() {} function f2() {} return { public_method1: f1, public_method2: f2 }; }());
擴展:
緊耦合擴展:
有時我們要求在擴展時調用以前已被定義的方法,這也有可能被用於覆蓋已有的方法。這時,對模塊的定義順序是有要求的。
var klm = (function (my) { var old_moduleMethod = my.moduleMethod; my.moduleMethod = function () { // 方法重載 // 可通過 old_moduleMethod 調用以前的方法... }; return my; }(klm));
克隆與繼承:
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()
var MODULE_TWO = (function (old) { var my = Object.create(old); var super_moduleMethod = old.moduleMethod; my.moduleMethod = function () { // override method ... }; return my; }(MODULE));
與其它模塊規范或 JS 庫的適配:
模塊環境探測:
現今,CommonJS Modules 與 AMD 有着廣泛的應用,如果確定 AMD 的 define 是可用的,我們當然可以使用 define 來編寫模塊化的代碼。然而,我們不能假定我們的代碼必然運行於 AMD 環境下,有沒有辦法可以讓我們的代碼?
其實我們只需要在某個地方加上對 CommonJS Modules 與 AMD 的探測並根據探測結果來“注冊”自己就可以了,以上那些模塊模式仍然有用。AMD 定義了 define 函數,我們可以使用 typeof 探測該函數是否已定義。若要更嚴格一點,可以繼續判斷 define.amd 是否有定義。另外,SeaJS 也使用了 define 函數,但和 AMD 的 define 又不太一樣。對於 CommonJS,可以檢查 exports 或是 module.exports 是否有定義。
var klm = (function () { var my = {}; // 代碼 ... if (typeof define == 'function') { define( function(){ return my; } ); }else if (typeof module != 'undefined' && module.exports) { module.exports = my; } return my; }());
其它一些 JS 庫的做法
jquery的檢測方式
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; }
接下來看看多重匿名函數的做法:
(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 的做法
(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 對象。
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'; };