JavaScript之模塊化編程

前言

模塊是任何大型應用程序架構中不可缺乏的一部分,模塊可使咱們清晰地分離和組織項目中的代碼單元。在項目開發中,經過移除依賴,鬆耦合可使應用程序的可維護性更強。與其餘傳統編程語言不一樣,在當前JavaScript裏,並無提供原生的、有組織性的引入模塊方式。本文就來探討一下目前的常見幾種模塊化解決方案。html

1.對象字面量表示法

對象字面量能夠認爲是包含一組鍵值對的對象,每一對鍵和值由冒號分隔。對象字面量不須要使用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

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模塊格式自己就是對定義模塊的建議,其模塊和依賴均可以進行異步加載,並且具備高度的靈活性,清除了代碼和模塊之間可能慣有的緊耦合。app

關於AMD有兩個很是重要的概念,那就是用於模塊定義的define方法和用於處理依賴加載的require方法。curl

做爲一個規範,只需定義其語法API,而不關心其實現。define函數定義以下:
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較爲穩妥。

寫在最後

本文論述了幾種模塊化編程的方式,它們各有優劣,各有適用場景。但願在之後的編程實踐中,選擇合適的模式,不斷提升代碼的可讀性、可維護性和可擴展性。

相關文章
相關標籤/搜索