來自:http://www.cnblogs.com/syfwhu/p/4883532.html
前言
模塊是任何大型應用程序架構中不可缺少的一部分,模塊可以使我們清晰地分離和組織項目中的代碼單元。在項目開發中,通過移除依賴,鬆耦合可以使應用程序的可維護性更強。與其他傳統編程語言不同,在當前JavaScript裏,並沒有提供原生的、有組織性的引入模塊方式。本文就來探討一下目前的常見幾種模塊化解決方案。
1.對象字面量表示法
對象字面量可以認爲是包含一組鍵值對的對象,每一對鍵和值由冒號分隔。對象字面量不需要使用new運算符進行實例化,在對象的外部也可以給對象添加屬性和方法。示例如下:
1 var myModule = { 2 myProperty: "jeri", 3 4 // 對象字面量可以包含屬性和方法 5 // 例如,可以聲明模塊的配置對象 6 myConfig: { 7 useCaching: true, 8 language: "en" 9 }, 10 11 // 基本方法 12 myMethod1: function () { 13 console.log("method1"); 14 }, 15 16 // 根據當前配置輸出信息 17 myMethod2: function () { 18 console.log("Caching is:" + '(this.myConfig.useCaching) ? "enabled" : "disabled"'); 19 }, 20 21 // 根據當前配置輸出信息 22 myMethod3: function (newConfig) { 23 24 if (typeof newConfig === "object") { 25 this.myConfig = newConfig; 26 console.log(this.myConfig.language); 27 } 28 } 29 }
如上所述,使用對象字面量有助於封裝和組織代碼,然後不同的對象字面量模塊再構成複雜的項目。
2.Module模式
Module模式最初定義在傳統的軟件工程中,爲類提供私有和公有封裝的方法。在JavaScript中,並不能可以直接聲明類,但我們可以使用閉包來封裝私有的屬性和方法,進而模擬類的概念,在JavaScript中實現Module模式。通過這種方式,我們就使得一個單獨的對象擁有公有/私有方法和變量,從而屏蔽來自全局作用域的特殊部分,也就大大降低了變量聲明之間和函數聲明之間衝突的可能性。
1 var myModule = (function () { 2 3 // 私有變量 4 var privateVar = 0; 5 6 // 私有函數 7 var privateFun = function (foo) { 8 console.log(foo); 9 }; 10 11 return { 12 // 私有變量 13 publicVar: "foo", 14 15 // 公有函數 16 publicFun: function (arg) { 17 18 // 修改私有變量 19 privateVar ++; 20 21 // 傳入bar調用私有方法 22 privateFun(arg); 23 } 24 }; 25 }) ();
如上所示,通過使用閉包我們封裝了私有變量和方法,而只暴露了一個接口供其他部分調用。私有變量(privateVar)和方法(privateFun)被侷限於模塊的閉包之中,只有通過公有方法才能訪問。該模式除了返回的是一個對象而不是一個函數之外,非常類似於一個立即調用函數表達式,我們可以爲返回的對象添加新的屬性和方法,這些新增的屬性和方法對外部調用者來說都是可用的。
Module模式的這種JavaScript實現對於具有面向對象開發經驗的人來說非常簡潔,但其也有自身的缺點和劣勢。
由於我們訪問公有和私有成員的方式不同,當我們想改變可見性時,我們需要修改每一個曾經使用該成員的地方,並不利於維護和升級,耦合度並不理想。而且,在之後新添加的方法裏,我們並不能訪問以前聲明的私有方法和變量,因爲閉包只在創建時完成綁定。我們也無法爲私有方法創建自動化單元測試,修正私有方法也是極其困難的,我們需要複寫所有與私有方法交互的公有方法,bug修正時工作量會很大。另外,我們也不能輕易的擴展私有方法。
關於腳本加載器
要討論 AMD 和 CommonJS 模塊,我們必然會談及一個顯而易見的話題——腳本加載器。目前,腳本加載是爲了讓我們能在現今的各種應用中都能使用模塊化的 JavaScript 這個目標而服務的。有很多加載器用於 AMD 和 CommonJS方式中的模塊加載,比較出名的有RequireJS 和 curl.js。關於腳本加載器的使用方式和運行機制,大家可以自行了解一下。
3.AMD模塊
AMD全稱是Asynchronous Module Definition,即異步模塊加載機制。它誕生於使用XHR+eval的Dojo開發經驗,其整體目標是提供模塊化的JavaScript解決方案,避免未來的任何解決方案受到過去解決方案缺點的影響。AMD模塊格式本身就是對定義模塊的建議,其模塊和依賴都可以進行異步加載,而且具有高度的靈活性,清除了代碼和模塊之間可能慣有的緊耦合。
關於AMD有兩個非常重要的概念,那就是用於模塊定義的define方法和用於處理依賴加載的require方法。
1 define( 2 [module-name?] /*可選*/, 3 [array-of-dependencies?] /*可選*/, 4 [module-factory-or-object] 5 );
- module-name: 模塊標識,可以省略。如果沒有這個屬性,則稱爲匿名模塊。
- array-of-dependencies: 所依賴的模塊,可以省略。
- module-factory-or-object: 模塊的實現,或者一個JavaScript對象。
具體示例如下:
1 define( 2 "myModule", 3 ["foo", "bar"], 4 5 // 模塊定義函數,依賴(foo,bar)作爲參數映射到函數上 6 function (foo, bar) { 7 // 創建模塊 8 var myModule = { 9 myFun: function () { 10 console.log("Jeri"); 11 } 12 } 13 14 // 返回定義的模塊 15 return myModule; 16 } 17 );
require用於加載JavaScript文件或模塊的代碼,獲取依賴。示例如下:
1 // foo,bar爲外部模塊,加載以後的輸出作爲回調函數的參數傳入,以便訪問 2 requrie(["foo", "bar"], function (foo, bar) { 3 4 // 其他代碼 5 foo.doSomething(); 6 });
下面是一個動態加載依賴的示例:
1 define( 2 function (requrie) { 3 var isReady = false, 4 foobar; 5 6 requrie(["foo", "bar"], function (foo, bar) { 7 isReady = true, 8 foobar = foo() + bar(); 9 }); 10 11 // 返回定義的模塊 12 return { 13 isReady: isReady, 14 foobar: foobar 15 }; 16 } 17 );
AMD模塊可以使用插件,也就是說當我們加載依賴時,可以加載任意格式的文件。AMD對於如何完成靈活模塊的定義提供了明確的建議,使用AMD編寫模塊化的JS代碼,比現有的全局命名空間和<script>標籤解決方案更加簡潔,沒有全局命名空間污染,在需要的時候也可以延遲加載腳本。
4.CommonJS模塊
CommonJS規範建議指定一個簡單的API來聲明在瀏覽器外部工作的模塊。與AMD不同,它試圖包含更廣泛的引人關注的問題,如IO、文件系統等。
從結構來看,CommonJS模塊是JS中可以複用的部分,導出特定對象,以便可以用於任何依賴代碼。與AMD表現形式不同的是,CommonJS模塊並不使用define進行定義。CommonJS模塊由兩部分組成:變量exports和require函數。exports包含了一個模塊希望其他模塊能夠使用的對象,require函數用來導入其他模塊的導出,也就是用來加載其他模塊依賴。示例如下:
lib.js
1 // 新定義的模塊方法 2 function log(arg) { 3 console.log(arg); 4 } 5 6 // 把方法暴露給其他模塊 7 exports.log = log;
app.js
1 // ./lib是我們需要的一個依賴 2 var lib = requrie("./lib"); 3 4 // 新定義的模塊方法 5 function foo() { 6 lib.log("jeri"); 7 } 8 9 // 把方法暴露給其他模塊 10 exports.foo = foo;
雖然在瀏覽器端可以使用CommonJS組織模塊,但有不少開發者認爲CommonJS更適合於服務器端開發,因爲很多CommonJS API具有面向服務器的特性,如io、system等。NodeJs使用的就是CommonJS規範。當一個模塊可能用於服務器端時,一些開發人員傾向於選擇CommonJS,其他情況下使用AMD。
AMD模塊可以使用插件,也就是說當我們加載依賴時,可以加載任意格式的文件,並且可以定義更細粒度的東西,如構造函數和函數,但CommonJS模塊僅能定義不易使用的對象。在模塊的定義和引入方面,二者也有很大的不同。AMD和CommonJS都是非常優秀的模塊模式,各自有不同的目標。
- AMD採用採用瀏覽器優先的開發方法,選擇異步行爲和簡化的後向兼容性,但沒有任何的文件I/O概念。支持對象、函數、構造函數以及其他類型的對象,在瀏覽器中原生運行。
- CommonJS採用服務器優先的方法,假定同步行爲,沒有全局概念負擔,僅將對象作爲模塊給予支持。CommonJS支持非包裝模塊,更接近下一代ES Harmony規範。
5.ES Harmony模塊
TC39——負責制定 ECMAScript 語法和語義以及其未來迭代的標準團體,在近幾年一直在密切關注 JavaScript 在大規模開發中的使用情況的演進,而且也敏感地意識到了需要有更好的語言特性來編寫更加模塊化的 JS。基於這個原因,目前有提案已經提出了一系列令人振奮的對語言的補充。雖然Harmony還處於建議階段,但我們可以先一覽新的接口特性。
Imports和Exports模塊
在ES.next中,已經爲模塊依賴和模塊導出提供了更加簡潔的方式,那就是import和export。
- import聲明綁定一個模塊,作爲局部變量導出,並能被重命名,以避免名稱衝突。
- export聲明一個外部可見模塊的本地綁定,其它模塊能夠讀取這些導出,但無法進行修改。模塊可以導出子模塊,但不能導出再其他地方定義的模塊。導出也是可以重命名的。
cakes.js
1 module staff { 2 // 指定導出 3 export var baker = { 4 bake: function(item) { 5 console.log('Woo! I just baked ' + item); 6 } 7 } 8 }; 9 10 module skills { 11 export var specialty = "baking"; 12 export var experience = "5 years"; 13 }; 14 15 module cakeFactory { 16 // 指定依賴項 17 import baker from staff; 18 19 // 通過通配符導入所有東西 20 import * from skills; 21 22 export var oven = { 23 makeCupcake: function(toppings) { 24 baker.bake('cupcake', toppings); 25 }, 26 makeMuffin: function(mSize) { 27 baker.bake('muffin', size); 28 } 29 } 30 };
遠程加載模塊
在ES.next裏還建議支持遠程模塊加載,示例如下:
1 module cakeFactory from 'http://****/cakes.js'; 2 3 cakeFactory.oven.makeCupcake('sprinkles'); 4 5 cakeFactory.oven.makeMuffin('large');
模塊加載器 API
模塊加載器建議一個動態的API在嚴格控制的上下問中加載模塊。加載器支持的特徵包括用來加載模塊的load( url, moduleInstance,
error)
,以及createModule( object, globalModuleReferences)
等等。
用於服務器的類 CommonJS 模塊
對於面向服務器的開發者來說,在 ES.next 中提出的模塊系統並非侷限於對瀏覽器端模塊的關注。例如下面是一個在服務器端使用的類CommonJS模塊:
File.js
1 // io/File.js 2 export function open(path) { 3 // ... 4 }; 5 export function close(hnd) { 6 // ... 7 };
LexicalHandler.js
1 // compiler/LexicalHandler.js 2 module file from 'io/File'; 3 4 import {open, close} from file; 5 export function scan( in ) { 6 try { 7 var h = open( in )... 8 } finally { 9 close(h) 10 } 11 }
app.js
1 module lexer from 'compiler/LexicalHandler'; 2 module stdlib from '@std'; 3 4 // ... scan(cmdline[0]) ...
ES Harmony有了很多令人振奮的新功能加入,以求簡化應用程序的開發,並處理依賴管理等問題。然而,至今爲止,還沒有形成新的規範,並不能得到衆多瀏覽器的支持。目前,要想使用Harmony語法的最佳選擇是通過transpiler,如谷歌的Traceur或Esprima。在新規範發佈之前,我們還是選擇AMD和CommonJS較爲穩妥。
寫在最後
本文論述了幾種模塊化編程的方式,它們各有優劣,各有適用場景。希望在以後的編程實踐中,選擇合適的模式,不斷提高代碼的可讀性、可維護性和可擴展性。