模塊是任何大型應用程序架構中不可缺乏的一部分,模塊可使咱們清晰地分離和組織項目中的代碼單元。在項目開發中,經過移除依賴,鬆耦合可使應用程序的可維護性更強。與其餘傳統編程語言不一樣,在當前JavaScript裏,並無提供原生的、有組織性的引入模塊方式。本文就來探討一下目前的常見幾種模塊化解決方案。html
對象字面量能夠認爲是包含一組鍵值對的對象,每一對鍵和值由冒號分隔。對象字面量不須要使用new運算符進行實例化,在對象的外部也能夠給對象添加屬性和方法。示例以下:git
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 }
如上所述,使用對象字面量有助於封裝和組織代碼,而後不一樣的對象字面量模塊再構成複雜的項目。github
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。關於腳本加載器的使用方式和運行機制,你們能夠自行了解一下。架構
AMD全稱是Asynchronous Module Definition,即異步模塊加載機制。它誕生於使用XHR+eval的Dojo開發經驗,其總體目標是提供模塊化的JavaScript解決方案,避免將來的任何解決方案受到過去解決方案缺點的影響。AMD模塊格式自己就是對定義模塊的建議,其模塊和依賴均可以進行異步加載,並且具備高度的靈活性,清除了代碼和模塊之間可能慣有的緊耦合。app
關於AMD有兩個很是重要的概念,那就是用於模塊定義的define方法和用於處理依賴加載的require方法。curl
1 define( 2 [module-name?] /*可選*/, 3 [array-of-dependencies?] /*可選*/, 4 [module-factory-or-object] 5 );
具體示例以下:
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>標籤解決方案更加簡潔,沒有全局命名空間污染,在須要的時候也能夠延遲加載腳本。
CommonJS規範建議指定一個簡單的API來聲明在瀏覽器外部工做的模塊。與AMD不一樣,它試圖包含更普遍的引人關注的問題,如IO、文件系統等。
從結構來看,CommonJS模塊是JS中能夠複用的部分,導出特定對象,以即可以用於任何依賴代碼。與AMD表現形式不一樣的是,CommonJS模塊並不使用define進行定義。CommonJS模塊由兩部分組成:變量exports和require函數。exports包含了一個模塊但願其餘模塊可以使用的對象,require函數用來導入其餘模塊的導出,也就是用來加載其餘模塊依賴。示例以下:
1 // 新定義的模塊方法 2 function log(arg) { 3 console.log(arg); 4 } 5 6 // 把方法暴露給其餘模塊 7 exports.log = log;
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都是很是優秀的模塊模式,各自有不一樣的目標。
TC39——負責制定 ECMAScript 語法和語義以及其將來迭代的標準團體,在近幾年一直在密切關注 JavaScript 在大規模開發中的使用狀況的演進,並且也敏感地意識到了須要有更好的語言特性來編寫更加模塊化的 JS。基於這個緣由,目前有提案已經提出了一系列使人振奮的對語言的補充。雖然Harmony還處於建議階段,但咱們能夠先一覽新的接口特性。
在ES.next中,已經爲模塊依賴和模塊導出提供了更加簡潔的方式,那就是import和export。
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在嚴格控制的上下問中加載模塊。加載器支持的特徵包括用來加載模塊的load( url, moduleInstance, error)
,以及createModule( object, globalModuleReferences)
等等。
對於面向服務器的開發者來講,在 ES.next 中提出的模塊系統並不是侷限於對瀏覽器端模塊的關注。例以下面是一個在服務器端使用的類CommonJS模塊:
1 // io/File.js 2 export function open(path) { 3 // ... 4 }; 5 export function close(hnd) { 6 // ... 7 };
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 }
1 module lexer from 'compiler/LexicalHandler'; 2 module stdlib from '@std'; 3 4 // ... scan(cmdline[0]) ...
ES Harmony有了不少使人振奮的新功能加入,以求簡化應用程序的開發,並處理依賴管理等問題。然而,至今爲止,尚未造成新的規範,並不能獲得衆多瀏覽器的支持。目前,要想使用Harmony語法的最佳選擇是經過transpiler,如谷歌的Traceur或Esprima。在新規範發佈以前,咱們仍是選擇AMD和CommonJS較爲穩妥。
本文論述了幾種模塊化編程的方式,它們各有優劣,各有適用場景。但願在之後的編程實踐中,選擇合適的模式,不斷提升代碼的可讀性、可維護性和可擴展性。