天天看點

《JavaScript設計模式》——9.2 Module(子產品)模式

本節書摘來自異步社群《javascript設計模式》一書中的第9章,第9.2節, 作者: 【美】addy osmani 譯者: 徐濤 更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

子產品是任何強大應用程式架構中不可或缺的一部分,它通常能夠幫助我們清晰地分離群組織項目中的代碼單元。

在javascript中,有幾種用于實作子產品的方法,包括:

對象字面量表示法

module模式

amd子產品

commonjs子產品

ecmascript harmony子產品

我們稍後将在本書第11章探索後三種方法。module模式在某種程度上是基于對象字面量,是以首先重新認識對象字面量是有意義的。

9.2.1 對象字面量

在對象字面量表示法中,一個對象被描述為一組包含在大括号({})中、以逗号分隔的name/value對。對象内的名稱可以是字元串或辨別符,後面跟着一個冒号。對象中最後的一個name/value對的後面不用加逗号,如果加逗号将會導緻出錯。

對象字面量不需要使用new運算符進行執行個體化,但不能用在一個語句的開頭,因為開始的可能被解讀為一個塊的開始。在對象的外部,新成員可以使用如下指派語句添加到對象字面量上,如:mymodule.property = "somevalue";

下面我們可以看到一個更完整的示例:使用對象字面量表示法定義的子產品:

使用對象字面量有助于封裝群組織代碼,如果想進一步了解有關對象字面量的資訊,麗貝卡·墨菲曾對這一主題進行了深入解析,可閱讀其文章進行了解。

也就是說,如果我們選擇了這種技術,我們可能同樣也對module模式感興趣。它仍然使用對象字面量,但隻是作為一個作用域函數的傳回值。

9.2.2 module(子產品)模式

module模式最初被定義為一種在傳統軟體工程中為類提供私有和公有封裝的方法。

在javascript中,module模式用于進一步模拟類的概念,通過這種方式,能夠使一個單獨的對象擁有公有/私有方法和變量,進而屏蔽來自全局作用域的特殊部分。産生的結果是:函數名與在頁面上其他腳本定義的函數沖突的可能性降低(見圖9-2)。

《JavaScript設計模式》——9.2 Module(子產品)模式

9.2.2.1 私有

module模式使用閉包封裝“私有”狀态群組織。它提供了一種包裝混合公有/私有方法和變量的方式,防止其洩露至全局作用域,并與别的開發人員的接口發生沖突。通過該模式,隻需傳回一個公有api,而其他的一切則都維持在私有閉包裡。

這為我們提供了一個屏蔽處理底層事件邏輯的整潔解決方案,同時隻暴露一個接口供應用程式的其他部分使用。該模式除了傳回一個對象而不是一個函數之外,非常類似于一個立即調用的函數表達式1。

應該指出的是,在javascript中沒有真正意義上的“私有”,因為不像有些傳統語言,javascript沒有通路修飾符。從技術上來說,我們不能稱變量為公有或是私有,是以我們需使用函數作用域來模拟這個概念。在module模式内,由于閉包的存在,聲明的變量和方法隻在該模式内部可用。但在傳回對象上定義的變量和方法,則對外部使用者都是可用的。

9.2.2.2 曆史

從曆史的角度來看,module模式最初是在2003年由多人共同開發出來的,其中包括理查德•康佛德。後來由道格拉斯·克勞克福德在其講座中推廣開來。除此之外,如果你曾體驗過雅虎的yui庫,它的一些特性看起來可能相當熟悉,原因是在建立它們的元件時,module模式對yui有很大的影響。

9.2.2.3 示例

讓我們通過建立一個自包含的子產品來看一下module模式的實作。

//用法:

//增加計數器

// 檢查計數器值并重置

//輸出:1

在這裡,代碼的其他部分無法直接讀取incrementcounter()或resetcounter()。counter變量實際上是完全與全局作用域隔離的,是以它表現得就像是一個私有變量,它的存在被局限于子產品的閉包内,是以唯一能夠通路其作用域的代碼就是這兩個函數。上述方法進行了有效的命名空間設定,是以在測試代碼中,所有的調用都需要加上字首(如:“testmodule”)。

使用module模式時,可能會覺得它可以用來定義一個簡單的模闆來入門使用。下面是一個包含命名空間、公有和私有變量的module模式:

來看另一個示例,我們可以看到一個使用這種模式實作的購物車。子產品本身是完全自包含在一個被稱為basketmodule的全局變量中。子產品中的basket數組是私有的,是以應用程式的其他部分無法直接讀取它。它隻與子產品的閉包一起存在,是以能夠通路它的方法都是那些能夠通路其作用域的方法(即additem()、getitem()等)。

在該子產品中,可能已經注意到傳回了一個object。它會被自動指派給basketmodule,以便我們可以與它互動,如下所示:

// 輸出: 2

// 輸出: 0.8

// 不過,下面的代碼不會正常工作

// 輸出:undefined

// 因為basket自身沒有暴露在公有的api裡

// 下面的代碼也不會正常工作,因為basket隻存在于basketmodule閉包的作用域裡,而不是存在于傳回的公有對象裡

上述方法在basketmodule内部都屬于有效的命名空間設定。

請注意上面的basket子產品中的作用域函數是如何包裹在所有函數的周圍,然後調用并立即存儲傳回值。這有很多優點,包括:

隻有我們的子產品才能享有擁有私有函數的自由。因為它們不會暴露于頁面的其餘部分(隻會暴露于我們輸出的api),我們認為它們是真正的私有。

鑒于函數往往已聲明并命名,在試圖找到有哪些函數抛出異常時,這将使得在調試器中顯示調用堆棧變得更容易。

正如 t.j.crowder 在過去所指出的,根據環境,它還可以讓我們傳回不同的函數。在過去,我曾看到開發人員使用它來執行ua測試,進而針對ie在他們的子產品内提供一個代碼路徑,但我們現在可以很容易地選擇特征檢測來實作類似的目的。

9.2.3 module模式變化

9.2.3.1 引入混入

模式的這種變化示範了全局變量(如:jquery、underscore)如何作為參數傳遞給子產品的匿名函數。這允許我們引入它們,并按照我們所希望的為它們取個本地别名。

9.2.3.2 引出

下一個變化允許我們聲明全局變量,而不需實作它們,并可以同樣地支援上一個示例中的全局引入的概念。

9.2.3.3 工具包和特定架構的module模式實作

dojo。提供了一種和對象一起用的便利方法dojo.setobject()。其第一個參數是用點号分割的字元串,如myobj.parent.child,它在parent對象中引用一個稱為child的屬性,parent對象是在myobj内部定義。我們可以使用setobject()設定子級的值(比如屬性等),如果中間對象不存在的話,也可以通過點号分割将中間的字元作為中間對象進行建立。

例如,如果要将basket.core聲明為store名稱空間的對象,可以采用傳統的方法來實作,如下所示:

或者,使用dojo 1.7(amd相容的版本)和上述方法,如下所示:

extjs。對比那些使用sencha extjs的人,你的運氣會好一點,因為官方文檔包含了一些示例,示範了extjs架構下如何正确使用module模式。

在這裡,我們可以看到這樣的一個示例:如何定義一個名稱空間,然後填充一個包含私有和公有 api 的子產品。除了一些語義差異,它與如何在純javascript中實作module模式十分相近。

yui。同樣,在使用 yui3 建構應用程式時,我們也可以實作module模式。下面的示例在很大程度上基于由eric miraglia提出的原始yui module模式實作,但它又與純javascript版本截然不同。

jquery。有許多方式可以将非jquery插件代碼包裝在module模式中。如果子產品之間有多個共性,ben cherry之前建議過一種實作,在子產品模式内部子產品定義附件使用函數包裝器。

在下面的示例中,定義了library函數,它聲明一個新庫,并在建立新庫(即子產品)時将init函數自動綁定到document.ready。

9.2.3.4 優點

我們已經了解了單例模式如何有用,但為什麼module模式是一個好的選擇呢?首先,相比真正封裝的思想,它對于很多擁有面向對象背景的開發人員來說更加整潔,至少是從javascript的角度。

其次,它支援私有資料,是以,在module模式中,代碼的公有(public)部分能夠接觸私有部分,然而外界無法接觸類的私有部分。

9.2.3.5 缺點

module模式的缺點是:由于我們通路公有和私有成員的方式不同,當我們想改變可見性時,實際上我們必須要修改每一個曾經使用過該成員的地方。

我們也無法通路那些之後在方法裡添加的私有成員。也就是說,在很多情況下,如果正确使用,module模式仍然是相當有用的,肯定可以改進應用程式的結構。

其他缺點包括:無法為私有成員建立自動化單元測試,bug需要修正更新檔時會增加額外的複雜性。為私有方法打更新檔是不可能的。相反,我們必須覆寫所有與有bug的私有方法進行互動的公有方法。另外開發人員也無法輕易地擴充私有方法,是以要記住,私有方法并不像它們最初顯現出來的那麼靈活。

繼續閱讀