鍍金池/ 教程/ HTML/ 全面解析 Module 模式
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計(jì)模式之狀態(tài)模式
JavaScript 核心(晉級(jí)高手必讀篇)
設(shè)計(jì)模式之建造者模式
JavaScript 與 DOM(上)——也適用于新手
設(shè)計(jì)模式之中介者模式
設(shè)計(jì)模式之裝飾者模式
設(shè)計(jì)模式之模板方法
設(shè)計(jì)模式之外觀模式
強(qiáng)大的原型和原型鏈
設(shè)計(jì)模式之構(gòu)造函數(shù)模式
揭秘命名函數(shù)表達(dá)式
深入理解J avaScript 系列(結(jié)局篇)
執(zhí)行上下文(Execution Contexts)
函數(shù)(Functions)
《你真懂 JavaScript 嗎?》答案詳解
設(shè)計(jì)模式之適配器模式
設(shè)計(jì)模式之組合模式
設(shè)計(jì)模式之命令模式
S.O.L.I.D 五大原則之單一職責(zé) SRP
編寫高質(zhì)量 JavaScript 代碼的基本要點(diǎn)
求值策略
閉包(Closures)
對(duì)象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計(jì)模式之代理模式
變量對(duì)象(Variable Object)
S.O.L.I.D 五大原則之里氏替換原則 LSP
面向?qū)ο缶幊讨话憷碚?/span>
設(shè)計(jì)模式之單例模式
Function 模式(上篇)
S.O.L.I.D 五大原則之依賴倒置原則 DIP
設(shè)計(jì)模式之迭代器模式
立即調(diào)用的函數(shù)表達(dá)式
設(shè)計(jì)模式之享元模式
設(shè)計(jì)模式之原型模式
根本沒有“JSON 對(duì)象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實(shí)現(xiàn)
全面解析 Module 模式
對(duì)象創(chuàng)建模式(下篇)
設(shè)計(jì)模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開閉原則 OCP
設(shè)計(jì)模式之橋接模式
設(shè)計(jì)模式之策略模式
設(shè)計(jì)模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計(jì)模式之工廠模式

全面解析 Module 模式

簡(jiǎn)介

Module 模式是 JavaScript 編程中一個(gè)非常通用的模式,一般情況下,大家都知道基本用法,本文嘗試著給大家更多該模式的高級(jí)使用方式。

首先我們來看看 Module 模式的基本特征:

  1. 模塊化,可重用
  2. 封裝了變量和 function,和全局的 namaspace 不接觸,松耦合
  3. 只暴露可用 public 的方法,其它私有方法全部隱藏

關(guān)于 Module 模式,最早是由 YUI 的成員 Eric Miraglia 在 4 年前提出了這個(gè)概念,我們將從一個(gè)簡(jiǎn)單的例子來解釋一下基本的用法(如果你已經(jīng)非常熟悉了,請(qǐng)忽略這一節(jié))。

基本用法

先看一下最簡(jiǎn)單的一個(gè)實(shí)現(xiàn),代碼如下:

var Calculator = function (eq) {
    //這里可以聲明私有成員
    var eqCtl = document.getElementById(eq);
    return {
        // 暴露公開的成員
        add: function (x, y) {
            var val = x + y;
            eqCtl.innerHTML = val;
        }
    };
};

我們可以通過如下的方式來調(diào)用:

var calculator = new Calculator('eq');
calculator.add(2, 2);

大家可能看到了,每次用的時(shí)候都要 new 一下,也就是說每個(gè)實(shí)例在內(nèi)存里都是一份 copy,如果你不需要傳參數(shù)或者沒有一些特殊苛刻的要求的話,我們可以在最后一個(gè)}后面加上一個(gè)括號(hào),來達(dá)到自執(zhí)行的目的,這樣該實(shí)例在內(nèi)存中只會(huì)存在一份 copy,不過在展示他的優(yōu)點(diǎn)之前,我們還是先來看看這個(gè)模式的基本使用方法吧。

匿名閉包

匿名閉包是讓一切成為可能的基礎(chǔ),而這也是 JavaScript 最好的特性,我們來創(chuàng)建一個(gè)最簡(jiǎn)單的閉包函數(shù),函數(shù)內(nèi)部的代碼一直存在于閉包內(nèi),在整個(gè)運(yùn)行周期內(nèi),該閉包都保證了內(nèi)部的代碼處于私有狀態(tài)。

(function () {
    // ... 所有的變量和function都在這里聲明,并且作用域也只能在這個(gè)匿名閉包里
    // ...但是這里的代碼依然可以訪問外部全局的對(duì)象
}());

注意,匿名函數(shù)后面的括號(hào),這是 JavaScript 語言所要求的,因?yàn)槿绻悴宦暶鞯脑?,JavaScript 解釋器默認(rèn)是聲明一個(gè) function 函數(shù),有括號(hào),就是創(chuàng)建一個(gè)函數(shù)表達(dá)式,也就是自執(zhí)行,用的時(shí)候不用和上面那樣在 new 了,當(dāng)然你也可以這樣來聲明:

(function () {/* 內(nèi)部代碼 */})();

不過我們推薦使用第一種方式,關(guān)于函數(shù)自執(zhí)行,我后面會(huì)有專門一篇文章進(jìn)行詳解,這里就不多說了。

引用全局變量

JavaScript 有一個(gè)特性叫做隱式全局變量,不管一個(gè)變量有沒有用過,JavaScript 解釋器反向遍歷作用域鏈來查找整個(gè)變量的 var 聲明,如果沒有找到 var,解釋器則假定該變量是全局變量,如果該變量用于了賦值操作的話,之前如果不存在的話,解釋器則會(huì)自動(dòng)創(chuàng)建它,這就是說在匿名閉包里使用或創(chuàng)建全局變量非常容易,不過比較困難的是,代碼比較難管理,尤其是閱讀代碼的人看著很多區(qū)分哪些變量是全局的,哪些是局部的。

不過,好在匿名函數(shù)里我們可以提供一個(gè)比較簡(jiǎn)單的替代方案,我們可以將全局變量當(dāng)成一個(gè)參數(shù)傳入到匿名函數(shù)然后使用,相比隱式全局變量,它又清晰又快,我們來看一個(gè)例子:

(function ($, YAHOO) {
    // 這里,我們的代碼就可以使用全局的jQuery對(duì)象了,YAHOO也是一樣
} (jQuery, YAHOO));

現(xiàn)在很多類庫里都有這種使用方式,比如 jQuery 源碼。

不過,有時(shí)候可能不僅僅要使用全局變量,而是也想聲明全局變量,如何做呢?我們可以通過匿名函數(shù)的返回值來返回這個(gè)全局變量,這也就是一個(gè)基本的 Module 模式,來看一個(gè)完整的代碼:

var blogModule = (function () {
    var my = {}, privateName = "博客園";
    function privateAddTopic(data) {
        // 這里是內(nèi)部處理代碼
    }
    my.Name = privateName;
    my.AddTopic = function (data) {
        privateAddTopic(data);
    };
    return my;
} ());

上面的代碼聲明了一個(gè)全局變量 blogModule,并且?guī)в?2 個(gè)可訪問的屬性:blogModule.AddTopic 和 blogModule.Name,除此之外,其它代碼都在匿名函數(shù)的閉包里保持著私有狀態(tài)。同時(shí)根據(jù)上面?zhèn)魅肴肿兞康睦樱覀円部梢院芊奖愕貍魅肫渌娜肿兞俊?/p>

高級(jí)用法

上面的內(nèi)容對(duì)大多數(shù)用戶已經(jīng)很足夠了,但我們還可以基于此模式延伸出更強(qiáng)大,易于擴(kuò)展的結(jié)構(gòu),讓我們一個(gè)一個(gè)來看。

擴(kuò)展

Module 模式的一個(gè)限制就是所有的代碼都要寫在一個(gè)文件,但是在一些大型項(xiàng)目里,將一個(gè)功能分離成多個(gè)文件是非常重要的,因?yàn)榭梢远嗳撕献饕子陂_發(fā)。再回頭看看上面的全局參數(shù)導(dǎo)入例子,我們能否把 blogModule 自身傳進(jìn)去呢?答案是肯定的,我們先將 blogModule 傳進(jìn)去,添加一個(gè)函數(shù)屬性,然后再返回就達(dá)到了我們所說的目的,上代碼:

var blogModule = (function (my) {
    my.AddPhoto = function () {
        //添加內(nèi)部代碼  
    };
    return my;
} (blogModule)); 

這段代碼,看起來是不是有 C#里擴(kuò)展方法的感覺?有點(diǎn)類似,但本質(zhì)不一樣哦。同時(shí)盡管 var 不是必須的,但為了確保一致,我們?cè)俅问褂昧怂?,代碼執(zhí)行以后,blogModule 下的 AddPhoto 就可以使用了,同時(shí)匿名函數(shù)內(nèi)部的代碼也依然保證了私密性和內(nèi)部狀態(tài)。

松耦合擴(kuò)展

上面的代碼盡管可以執(zhí)行,但是必須先聲明 blogModule,然后再執(zhí)行上面的擴(kuò)展代碼,也就是說步驟不能亂,怎么解決這個(gè)問題呢?我們來回想一下,我們平時(shí)聲明變量都是這樣的:

var cnblogs = cnblogs || {} ;

這是確保 cnblogs 對(duì)象,在存在的時(shí)候直接用,不存在的時(shí)候直接賦值為{},我們來看看如何利用這個(gè)特性來實(shí)現(xiàn) Module 模式的任意加載順序:

var blogModule = (function (my) {
    // 添加一些功能       
    return my;
} (blogModule || {}));  

通過這樣的代碼,每個(gè)單獨(dú)分離的文件都保證這個(gè)結(jié)構(gòu),那么我們就可以實(shí)現(xiàn)任意順序的加載,所以,這個(gè)時(shí)候的 var 就是必須要聲明的,因?yàn)椴宦暶?,其它文件讀取不到哦。

緊耦合擴(kuò)展

雖然松耦合擴(kuò)展很牛叉了,但是可能也會(huì)存在一些限制,比如你沒辦法重寫你的一些屬性或者函數(shù),也不能在初始化的時(shí)候就是用 Module 的屬性。緊耦合擴(kuò)展限制了加載順序,但是提供了我們重載的機(jī)會(huì),看如下例子:

var blogModule = (function (my) {
    var oldAddPhotoMethod = my.AddPhoto;
    my.AddPhoto = function () {
        // 重載方法,依然可通過oldAddPhotoMethod調(diào)用舊的方法
    };
    return my;
} (blogModule));

通過這種方式,我們達(dá)到了重載的目的,當(dāng)然如果你想在繼續(xù)在內(nèi)部使用原有的屬性,你可以調(diào)用 oldAddPhotoMethod 來用。

克隆與繼承

var blogModule = (function (old) {
    var my = {},
        key;
    for (key in old) {
        if (old.hasOwnProperty(key)) {
            my[key] = old[key];
        }
    }
    var oldAddPhotoMethod = old.AddPhoto;
    my.AddPhoto = function () {
        // 克隆以后,進(jìn)行了重寫,當(dāng)然也可以繼續(xù)調(diào)用oldAddPhotoMethod
    };
    return my;
} (blogModule));

這種方式靈活是靈活,但是也需要花費(fèi)靈活的代價(jià),其實(shí)該對(duì)象的屬性對(duì)象或 function 根本沒有被復(fù)制,只是對(duì)同一個(gè)對(duì)象多了一種引用而已,所以如果老對(duì)象去改變它,那克隆以后的對(duì)象所擁有的屬性或 function 函數(shù)也會(huì)被改變,解決這個(gè)問題,我們就得是用遞歸,但遞歸對(duì) function 函數(shù)的賦值也不好用,所以我們?cè)谶f歸的時(shí)候 eval 相應(yīng)的 function。不管怎么樣,我還是把這一個(gè)方式放在這個(gè)帖子里了,大家使用的時(shí)候注意一下就行了。

跨文件共享私有對(duì)象

通過上面的例子,我們知道,如果一個(gè) module 分割到多個(gè)文件的話,每個(gè)文件需要保證一樣的結(jié)構(gòu),也就是說每個(gè)文件匿名函數(shù)里的私有對(duì)象都不能交叉訪問,那如果我們非要使用,那怎么辦呢? 我們先看一段代碼:

var blogModule = (function (my) {
    var _private = my._private = my._private || {},       
        _seal = my._seal = my._seal || function () {
            delete my._private;
            delete my._seal;
            delete my._unseal;           
        },
        _unseal = my._unseal = my._unseal || function () {
            my._private = _private;
            my._seal = _seal;
            my._unseal = _unseal;
        };      
    return my;
} (blogModule || {}));

任何文件都可以對(duì)他們的局部變量_private 設(shè)屬性,并且設(shè)置對(duì)其他的文件也立即生效。一旦這個(gè)模塊加載結(jié)束,應(yīng)用會(huì)調(diào)用 blogModule._seal()"上鎖",這會(huì)阻止外部接入內(nèi)部的_private。如果這個(gè)模塊需要再次增生,應(yīng)用的生命周期內(nèi),任何文件都可以調(diào)用_unseal() ”開鎖”,然后再加載新文件。加載后再次調(diào)用 _seal()”上鎖”。

子模塊

最后一個(gè)也是最簡(jiǎn)單的使用方式,那就是創(chuàng)建子模塊

blogModule.CommentSubModule = (function () {
    var my = {};
    // ...
    return my;
} ());

盡管非常簡(jiǎn)單,我還是把它放進(jìn)來了,因?yàn)槲蚁胝f明的是子模塊也具有一般模塊所有的高級(jí)使用方式,也就是說你可以對(duì)任意子模塊再次使用上面的一些應(yīng)用方法。

總結(jié)

上面的大部分方式都可以互相組合使用的,一般來說如果要設(shè)計(jì)系統(tǒng),可能會(huì)用到松耦合擴(kuò)展,私有狀態(tài)和子模塊這樣的方式。另外,我這里沒有提到性能問題,但我認(rèn)為 Module 模式效率高,代碼少,加載速度快。使用松耦合擴(kuò)展允許并行加載,這更可以提升下載速度。不過初始化時(shí)間可能要慢一些,但是為了使用好的模式,這是值得的。