1. 程式人生 > 其它 >【類和模組】模組

【類和模組】模組

模組

將程式碼組織到類中的一個重要原因是,讓程式碼更加“模組化”,可以在很多不同場景中實現程式碼的重用。但類不是唯一的模組化程式碼的方式。一般來講,模組是一個獨立的JavaScript檔案。模組檔案可以包含一個類定義、一組相關的類、一個實用函式庫或者是一些待執行的程式碼。只要以模組的形式編寫程式碼,任何JavaScript程式碼段就可以當做一個模組。JavaScript中並沒有定義用以支援模組的語言結構(但imports和exports的確是JavaScript保留的關鍵字,因此JavaScript的未來版本可能會支援),這也意味著在JavaScript中編寫模組化的程式碼更多的是遵循某一種編碼約定。

很多JavaScript庫和客戶端程式設計框架都包含一些模組系統。比如,Dojo工具包和Google的Closure庫定義了provide()require()函式,用以宣告和載入模組。並且,CommonJS伺服器端JavaScript標準規範(參照http://commonjs.org )建立了一個模組規範,後者同 樣使用require()函式。這種模組系統通常用來處理模組載入和依賴性管理。如果使用這些框架,則必須按照框架提供的模組編寫約定來定義模組。

模組化的目標是支援大規模的程式開發,處理分散源中程式碼的組裝,並且能讓程式碼正確執行,哪怕包含了作者所不期望出現的模組程式碼,也可以正確執行程式碼。為了做到這一點,不冋的模組必須避免修改全域性執行上下文,因此後續模組應當在它們所期望執行的原始(或接近原始)上下文中執行。這實際上意味著模組應當儘可能少地定義全域性標識,理想狀況是,所有模組都不應當定義超過一個(全域性標識)。接下來我們給出的一種簡單的方法可以做到這一點。

用做名稱空間的物件

在模組建立過程中避免汙染全域性變數的一種方法是使用一個物件作為名稱空間。它將函式和值作為名稱空間物件屬性儲存起來(可以通過全域性變數引用),而不是定義全域性函式和變數。

最頂層的名稱空間往往用來標識建立模組的作者或組織,並避免名稱空間的命名衝突。比如,Google的Closure庫在它的名稱空間goog.structs中定義了Set類。每個開發者都反轉網際網路域名的組成部分,這樣建立的名稱空間字首是全域性唯一的,一般不會被其他模組作者採用。

使用很長的名稱空間來匯入模組的方式非常重要,然而程式設計師往往往將整個模組匯入全域性名稱空間,而不是匯入(名稱空間中的某個)單獨的類。

var sets = com.davidflanagan.collections.sets;

按照約定,模組的檔名應當和名稱空間匹配。sets模組應當儲存在檔案sets.js中。如果這個模組使用名稱空間collections.sets,那麼這個檔案應當儲存在目錄collections/下(這個目錄還應當包含另一個檔案maps.js) 。並且使用名稱空間com.davidflanagan.collections.sets 的模組應當在檔案com/davidflanagan/collections/sets.js中。

作為私有名稱空間的函式

模組對外匯出一些公用API,這些API是提供給其他程式設計師使用的,它包括函式、類、屬性和方法。但模組的實現往往需要一些額外的輔助函式和方法,這些函式和方法並不需要在模組外部可見。

可以通過將模組定義在某個函式的內部來實現。在一個函式中定義的變數和函式都屬於函式的區域性成員,在函式的外部是不可見的。實際上,可以將這個函式作用域用做模組的私有名稱空間(有時稱為“模組函式”)。下例展示瞭如何使用“模組函式”來實現Set類:

例:模組函式中的Set類
// 宣告全域性變數Set,使用一個函式的返回值給它賦值
// 函式結束時緊跟的一對圓括號說明這個函式定義後立即執行
// 它的返回值將賦值給Set,而不是將這個函式賦值給Set
// 注意它是一個函式表示式,不是一條語句,因此函式"invocation"並沒有建立全域性變數
var Set = (function invocation() {
	
  function Set() { // 這個建構函式是區域性變數
	this.values = {};	// 這個物件的屬性用來儲存這個集合
	this.n = 0;	// 集合中值的個數	,
	this.add.apply (this, arguments); // 將所有的引數都新增至集合中
}

// 給Set.prototype定義例項方法
// 這裡省略了詳細程式碼
Set.prototype.contains = function(value) {
	// 注意我們呼叫了v2s(),而不是呼叫帶有笨重的字首的set._v2s()
	return this.values.has0wnProperty(v2s(value));
};
Set.prototype.size = function() { return this.n; };
Set.prototype.add = function() {/*...*/};
Set.prototype.remove = function() (/*...*/};
	Set.prototype.foreach = function(f, context) {/*...*/};

// 這裡是上面的方法用到的一些輔助函式和變數
// 它們不屬於模組的共有API,但它們都隱藏在這個函式作用域內
// 因此我們不必將它們定義為Set的屬性或使用下劃線作為其字首
function v2s(val) {/*...*/}
function objectId(o) {/*...*/}
var nextId = 1;
// 這個模組的共有API是Set()建構函式
// 我們需要把這個函式從私有名稱空間中匯出來
// 以便在外部也可以使用它,在這種情況下,我們通過返回這個建構函式來匯出它
// 它變成第一行程式碼所指的表示式的值
return Set;
} ()); // 定義函式後立即執行

注意,這裡使用了立即執行的匿名函式,這在JavaScript中是一種慣用法。如果想讓程式碼在一個私有名稱空間中執行,只須給這段程式碼加上字首"(function(){ "和字尾" } ())"。開始的左圓括號確保這是一個函式表示式,而不是函式定義語句,因此可以給該字首新增一個函式名來讓程式碼變得更加清晰。在上例中使用了名字“invocation”,用以強調這個函式應當在定義之後立即執行。名字“namespace”也可以用來強調這個函式被用做名稱空間。

一旦將模組程式碼封裝進一個函式,就需要一些方法匯出其公用API,以便在模組函式的外部呼叫它們。在上例中,模組函式返回建構函式,這個建構函式隨後賦值給一個全域性變數。將值返回已經清楚地表明API已經匯出在函式作用域之外。如果模組API包含多個單元,則它可以返回名稱空間物件。對於sets模組來說,可以將程式碼寫成這樣:

// 建立一個全域性變數用來存放集合相關的模組
var collections;
if (!collections) collections = {};

// 定義sets模組
collections.sets = (function namespace() {
	// 在這裡定義多種"集合"類,使用區域性變數和函式
	//......這裡省略很多程式碼......

	// 通過返回名稱空間物件將API匯出
	return {
		// 匯出的屬性名:區域性變數名字
		AbstractSet: AbstractSet,
		NotSet: NotSet,
		AbstractEnumerableSet: AbstractEnumerableSet,
		SingletonSet: SingletonSet, 
		AbstractWritableSet: AbstractWritableSet, 
		ArraySet: ArraySet
	};
}());

另外一種類似的技術是將模組函式當做建構函式,通過new來呼叫,通過將它們賦值給this來將其匯出:

var collections;
if (!collections) collections = {};
collections.sets = (new function namespace() {
	// ......這裡省略很多程式碼......

	// 將API匯出至this物件
	this.AbstractSet = AbstractSet;
	this.NotSet = NotSet;		// ...... 	
	
	// 注意,這裡沒有返回值
}());

作為一種替代方案,如果已經定義了全域性名稱空間物件,這個模組函式可以直接設定那個物件的屬性,不用返回任何內容:

var collections;
if (!collections) collections = {};
collections.sets = {};
(function namespace() {
	// ......這裡省略很多程式碼......

	// 將共用API匯出到上面建立的名稱空間物件上
	collections.sets.AbstractSet = AbstractSet;
	collections.sets.NotSet = NotSet; // ......

	// 匯出的操作已經執行了,這裡不需要再寫return語句了
}());

有些框架實現了模組載入功能,其中包括其他一些匯出模組API的方法。比如,使用provides()函式來註冊其API,提供exports物件用以儲存模組API。由於JavaScript 目前還不具備模組管理的能力,因此應當根據所使用的框架和工具包來選擇合適的模組建立和匯出API的方式。