模塊模式

The Module Pattern

Modules

模塊是任何健壯的應(yīng)用程序架構(gòu)的一個完整部分肤无,并且通常用來幫助保持一個項目代碼單元既整潔又有條理。

在JS中遍烦,有如下幾個關(guān)于實現(xiàn)模塊的選項愈污,他們包括

1.模塊模式
2.對象字面量表示法
3.AMD 模塊
4.CommonJs 模塊
5.ECMAScript Harmony 模塊

我們將在后面探索上述的后三種模式回季。

模塊模式部分是基于對象字面量的,所以首先認識這個知識對我們來說是有意義的秕重。

對象字面量

對象字面量是一個對象不同,通常用一組包含在{}大括號中的鍵值對描述。在對象字面量中的名稱通常是后跟冒號的字符串或者標識符。
在對象的最后一組鍵值對之后二拐,不應(yīng)該有逗號服鹅,否則可能會導致錯誤。

var myObjectLiteral = {
    variableKey: variableValue,
    functionKey: function () {
      // ...
    }
};

對象字面量不需要使用關(guān)鍵詞new 運算符創(chuàng)建百新,但是也不應(yīng)該把它用于一段代碼聲明的開始企软,就像打開的花括號{通常被解釋為一個塊的開始。在對象之外饭望,一個新的屬性可以通過賦值的方式被添加進對象仗哨,例如 myModule.property = "SOME VALUE"

通過下面的例子我們可以看到一個通過復(fù)雜字面量定義的模塊的例子。

var myModule = {

  myProperty: "someValue",

  //對象字面量可以包含屬性和方法杰妓,例如我們可以定一個對象來表是這個模塊的配置
  myConfig: {
    useCaching: true,
    language: "en"
  },

  // 一個基本的方法
  saySomething: function () {
    console.log( "Where in the world is Paul Irish today?" );
  },

  // 一個基于現(xiàn)有配置進行輸出的方法
  reportMyConfig: function () {
    console.log( "Caching is: " + ( this.myConfig.useCaching ? "enabled" : "disabled") );
  },

  // 重寫現(xiàn)有配置的方法
  updateMyConfig: function( newConfig ) {

    if ( typeof newConfig === "object" ) {
      this.myConfig = newConfig;
      console.log( this.myConfig.language );
    }
  }
};

// =>: Where in the world is Paul Irish today?
myModule.saySomething();

// =>: Caching is: enabled
myModule.reportMyConfig();

// =>: fr
myModule.updateMyConfig({
  language: "fr",
  useCaching: false
});

// =>: Caching is: disabled
myModule.reportMyConfig();

利用對象字面量可以幫助你封裝和組織你的代碼藻治。如果我們選擇這種技術(shù),我們可能同樣會對模塊模式感興趣巷挥,它始終利用對象字面量桩卵,但僅僅是作為一個函數(shù)作用域的返回值。

The Module Pattern

模塊模式最初被定義為在傳統(tǒng)的軟件工程中為類定義提供公有和私有封裝的一種方式倍宾。
在JS中雏节,模塊模式被用來進一步模擬類的概念,通過在一個單例對象中包含公有的/私有的方法和變量高职,從全局作用域中屏蔽具體的細節(jié)部分钩乍。
這樣做的結(jié)果是我們減少了我們的函數(shù)名同該頁面上被定義的其他附加的函數(shù)沖突的可能性。

私有

模塊模式利用閉包來封裝私有的怔锌、狀態(tài)或者組織寥粹。它提供一種封裝公有和私有方法的混合方法,保護內(nèi)部的內(nèi)容不泄露到全局的作用域中并與其他的開發(fā)人員的接口相沖突埃元。在這種模式中涝涤,只有公有的方法會被返回,將所有其他的內(nèi)容都保留在了閉包內(nèi)岛杀。

這給我們提供了一個簡潔的解決方案阔拳,屏蔽內(nèi)部復(fù)雜邏輯并僅僅暴露我們的應(yīng)用其他部分希望使用的接口,該模式和立即被調(diào)用的函數(shù)表達式非常相似类嗤,除了返回一個對象而不是一個函數(shù)糊肠。

需要注意的是,js內(nèi)部確實沒有真正意義上的私有遗锣,因為它不像一些傳統(tǒng)的語言货裹,它沒有訪問修飾符。變量技術(shù)上不可以直接被聲明為共有的或者私有的黄伊,所以我們使用函數(shù)作用域來模擬這個概念泪酱,在模塊模式內(nèi)部,被聲明的變量或者方法,只能在模塊內(nèi)部使用墓阀,這歸功于閉包毡惜。然而在返回的對象中定義被定義的變量或者方法可以被任何人使用。

History

從歷史的角度看斯撮,模塊模式起初是被一群包括Richard Cornford的人在2003年開發(fā)出來的经伙。后來被道格拉斯克羅克福德(Douglas Crockford)在他的演講中推廣。另一件小事是勿锅,如果你曾經(jīng)使用過Yahoo的YUI庫帕膜,其中的一些功能呈現(xiàn)出這種十分熟悉的原因是模塊模式對YUI創(chuàng)建他們的組件時有強烈的影響。

例子

讓我們通過創(chuàng)建一個獨立的模塊來看看如何實現(xiàn)模塊模式


var testModule = (function () {

  var counter = 0;

  return {

    incrementCounter: function () {
      return counter++;
    },

    resetCounter: function () {
      console.log( "counter value prior to reset: " + counter );
      counter = 0;
    }
  };

})();

// 用法

// 增加數(shù)量
testModule.incrementCounter();

// 測試結(jié)果
// =>: counter value prior to reset: 1
testModule.resetCounter();

這里溢十,代碼的其他部分不可以直接訪問incrementCounter()resetCounter()垮刹。這里的counter變量實際上是被我們從全局作用域屏蔽掉了,所以它看上去扮演的是私有變量——它的存在被限制在了模塊的閉包之中张弛,也因此我們能夠在模塊外直接訪問的代碼只有兩個函數(shù)荒典。我們的命名空間是有效的,因此在我們測試我們代碼的環(huán)節(jié)吞鸭,我們需要在任何調(diào)用的時候寺董,加入模塊名作為前綴。

當使用模塊模式時刻剥,我們可能會發(fā)現(xiàn)我們定義一個簡單的模板遮咖,對開始使用它是有用的。這有一個包含命名空間造虏,公有/私有變量的例子:


var myNamespace = (function () {

  var myPrivateVar, myPrivateMethod;

  // 一個私有的計數(shù)器變量
  myPrivateVar = 0;

  // 一個私有的方法御吞,可以打印任何輸入的變量
  myPrivateMethod = function( foo ) {
      console.log( foo );
  };

  return {

    // 一個公有的變量
    myPublicVar: "foo",

    // 一個利用私有變量的公有函數(shù)/方法
    myPublicFunction: function( bar ) {

        //增加我們的私有變量
        myPrivateVar++;

          //調(diào)用我們私有的方法
        myPrivateMethod( bar );

    }
  };

})();

來看另一個例子,如下我們可以看到一個使用這種模式實現(xiàn)的購物籃漓藕。這個模塊是完全獨立的在魄藕,在全局變量中被叫做basketModule。其中basket數(shù)組在模塊中保持私有狀態(tài)撵术,所以我們應(yīng)用中的其他部分是不可以直接讀取它的。它僅僅存在于模塊的閉包之中话瞧,并且在模塊外部能訪問的僅僅是它暴露出的方法嫩与,例如addItem(),getItem()

var basketModule = (function () {

  // privates

  var basket = [];

  function doSomethingPrivate() {
    //...
  }

  function doSomethingElsePrivate() {
    //...
  }

  // Return an object exposed to the public
  return {

    // Add items to our basket
    addItem: function( values ) {
      basket.push(values);
    },

    // Get the count of items in the basket
    getItemCount: function () {
      return basket.length;
    },

    // Public alias to a private function
    doSomething: doSomethingPrivate,

    // Get the total value of items in the basket
    getTotal: function () {

      var q = this.getItemCount(),
          p = 0;

      while (q--) {
        p += basket[q].price;
      }

      return p;
    }
  };
})();

你可能會注意到交排,在模塊內(nèi)部划滋,我們返回了一個object,他們會被自動的賦值給basketModule,以便我們可以按照如下的方式與之互動埃篓。

// basketModule 模塊返回了一個我們可以使用的公用API對象处坪。

basketModule.addItem({
  item: "bread",
  price: 0.5
});

basketModule.addItem({
  item: "butter",
  price: 0.3
});

// => 2
console.log( basketModule.getItemCount() );

// => 0.8
console.log( basketModule.getTotal() );

// 然而下面的方式將不會工作

// => undefined
// 這是因為這個屬性沒有被這個模塊作為公有的API暴露出來
console.log( basketModule.basket );

// 不工作,理由同上,只存在于閉包內(nèi)同窘,沒有被作為公有的API暴露出來玄帕。
console.log( basket );

以上方法在命名空間(basketModule)內(nèi)部都是有效的。

注意如何界定上面的basket模塊中包含的功能想邦,即我們之后立即調(diào)用并存儲的返回值裤纹。這里有一系列的優(yōu)點如下:

1.擁有僅供我們模塊使用的公有和私有成員的自由,因為它們沒有暴露在頁面的其他部分(除了我們暴露的公有API)丧没,它們可以被看作是私有的鹰椒。
2.函數(shù)被正常的聲明和命名,當我們試圖去發(fā)現(xiàn)哪個函數(shù)拋出異常時將容易在調(diào)試工具中看到函數(shù)調(diào)用棧呕童。
3.T. J Crowder指出漆际,在過去,它也使我們能夠在不同的環(huán)境中返回不同的功能夺饲。在過去奸汇,我已經(jīng)看到開發(fā)人員使用這個進行UA測試,為了針對IE瀏覽器在他們的模塊中提供了一個代碼路徑钞支,但在當下我們可以選擇特性針對完成相似的目標茫蛹。

Module Pattern Variations

Import mixins(導入混入?)

這種模式的變化烁挟,展示了如何將全局變量作為函數(shù)參數(shù)婴洼,傳遞給一個定義我們模塊的匿名函數(shù)。這允許我們有效的將它導入到我們代碼的作用域并且命名為我們希望的別名撼嗓。


//              此處可以起別名
var myModule = (function ( jQ, _ ) {

    function privateMethod1(){
        jQ(".container").html("test");
    }

    function privateMethod2(){
      console.log( _.min([10, 5, 100, 2, 1000]) );
    }

    return{
        publicMethod: function(){
            privateMethod1();
        }
    };

// 將jQuery 和 Underscore導入到匿名函數(shù)中
})( jQuery, _ );

myModule.publicMethod();

Exports(導出)

接下來的這個變化允許我們聲明全局變量而不去使用它柬采,有點像支持導入全局變量的概念,在接下來的例子中我們可以看到且警。


// Global module
var myModule = (function () {

  //模塊對象
  var module = {},
    privateVariable = "Hello World";

  function privateMethod() {
    // ...
  }

  module.publicProperty = "Foobar";
  module.publicMethod = function () {
    console.log( privateVariable );
  };

  return module;

})();

工具包和特定框架的模塊模式實現(xiàn)

Dojo

Dojo提供了一些便利的方法處理對象粉捻,通過對象的方法調(diào)用dojo.setObject()。用點分割的字符串斑芜,作為它的第一個參數(shù)肩刃,就像myObject.parent.child中一個“child"作為"parent"的一個屬性,而"parent"則定義在"myObj"中杏头。使用 setObject()允許我們?nèi)ピO(shè)置它的子對象盈包,如果在給定的路徑中對象不存在的話,創(chuàng)建中間的對象醇王。

例如你想創(chuàng)建一個baseket.core作為store命名空間下的對象呢燥,這可以用下面的一般性方式實現(xiàn)。

var store = window.store || {};

if ( !store["basket"] ) {
  store.basket = {};
}

if ( !store.basket["core"] ) {
  store.basket.core = {};
}

store.basket.core = {
  // ...rest of our logic
};

或者使用 Dojo 1.7(AMD兼容版)這個庫并配合如下代碼實現(xiàn)

require(["dojo/_base/customStore"], function( store ){

  // 使用 dojo.setObject()
  store.setObject( "basket.core", (function() {

      var basket = [];

      function privateMethod() {
          console.log(basket);
      }

      return {
          publicMethod: function(){
                  privateMethod();
          }
      };

  })());

});

如果想了解更多的關(guān)于dojo.setObject的信息寓娩,請查看官方文檔

ExtJS

關(guān)于那些使用了Sencha的ExtJs的叛氨,下面的一個例子告訴你呼渣,如何利用框架正確的使用模塊模式。

這里寞埠,我們看到了一個關(guān)于如何定義命名空間屁置,然后利用包含公有和私有的API模塊填充它的例子。除了一些語法不同外畸裳,它與VanillaJs中的模塊模式的實現(xiàn)非常接近缰犁。


// 創(chuàng)建命名空間
Ext.namespace("myNameSpace");

// 在命名空間中創(chuàng)建應(yīng)用
myNameSpace.app = function () {

  // 不要在這里創(chuàng)建引用DOM或者DOM元素

  // 私有變量
  var btn1,
      privVar1 = 11;

  // 私有方法
  var btn1Handler = function ( button, event ) {
      console.log( "privVar1=" + privVar1 );
      console.log( "this.btn1Text=" + this.btn1Text );
    };

  // 公有方法
  return {
    // 公有屬性
    btn1Text: "Button 1",

    // 公有方法
    init: function () {

      if ( Ext.Ext2 ) {

        btn1 = new Ext.Button({
          renderTo: "btn1-ct",
          text: this.btn1Text,
          handler: btn1Handler
        });

      } else {

        btn1 = new Ext.Button( "btn1-ct", {
          text: this.btn1Text,
          handler: btn1Handler
        });

      }
    }
  };
}();

YUI

相似的,當我們構(gòu)建我們的應(yīng)用程序的時候怖糊,也可以使用YUI3實現(xiàn)模塊模式帅容。
接下來的這個例子嚴重的依賴原來的YUI模塊模式,由Eric Miraglia實現(xiàn)伍伤,但是并徘,它與vanillaJs的版本存在著極大的不同。


Y.namespace( "store.basket" ) ;
Y.store.basket = (function () {

    var myPrivateVar, myPrivateMethod;

    // 私有變量:
    myPrivateVar = "I can be accessed only within Y.store.basket.";

    // 私有方法:
    myPrivateMethod = function () {
        Y.log( "I can be accessed only from within YAHOO.store.basket" );
    }

    return {
        myPublicProperty: "I'm a public property.",

        myPublicMethod: function () {
            Y.log( "I'm a public method." );

            // 在basket里面我們可以使用私有變量和方法
            Y.log( myPrivateVar );
            Y.log( myPrivateMethod() );

            // 通過this訪問該作用域(當前返回對象)里的方法
            Y.log( this.myPublicProperty );
        }
    };

})();

jQuery

這里有一些方法扰魂,jQuery代碼不指定插件也可以包裹在模塊模式中麦乞。Ben Cherry之前建議過一種實現(xiàn),將一些含有共同點的模塊劝评,通過包裹在函數(shù)中定義為一個模塊姐直。

在下面的例子中,一個library方法被定義蒋畜,它聲明了一個庫声畏,并在它被創(chuàng)建的時候,自動的將它的inti方法綁定到了document.ready方法中姻成。

function library( module ) {

  $( function() {
    if ( module.init ) {
      module.init();
    }
  });

  return module;
}

var myLibrary = library(function () {

  return {
    init: function () {
      // module implementation
    }
  };
}());

優(yōu)點

我們已經(jīng)看到了模塊模式為何是有用的插龄,但是為什么模塊模式是一個好的選擇?對于一個初學者來說科展,有著面向?qū)ο蟊尘暗囊粋€開發(fā)者來說均牢,相比真正的封裝而言,這樣會更整潔才睹,至少從JS角度來看是這樣徘跪。

其實,它支持私有數(shù)據(jù)琅攘,所以在模塊模式中真椿,我們的公有部分的代碼可以接觸到私有的部分,然而模塊外的世界則不可接觸到模塊中的私有部分乎澄。

缺點

我們訪問公有的成員和私有的成員方式不同,所以當我們想要改變成員的可訪問性時测摔,我們實際上需要修改每個用到模塊成員的地方置济。

我們也無法在方法中訪問之后被添加到對象的私有成員解恰。也就是說,在許多情況下模塊模式仍是非常有用的浙于,當使用正確护盈,當然有潛力干山我們的應(yīng)用程序結(jié)構(gòu)。

其他的缺點包括無法為私有成員創(chuàng)建單元測試羞酗,當錯誤需要熱修復(fù)時腐宋,增加了額外的復(fù)雜度。修復(fù)私有的地方簡直是不可能的檀轨。相反胸竞,一個人必須修改所有的與有bug的私有成員交互的公有成員,開發(fā)者也無法輕易的擴展私有成員参萄,所以值得記住的是私有成員并不像最初呈現(xiàn)的那樣靈活卫枝。

關(guān)于更多關(guān)于模塊模式的內(nèi)容,可以參考Ben Cherry的優(yōu)秀的深入的文章讹挎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末校赤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子筒溃,更是在濱河造成了極大的恐慌马篮,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怜奖,死亡現(xiàn)場離奇詭異浑测,居然都是意外死亡,警方通過查閱死者的電腦和手機烦周,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門尽爆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人读慎,你說我怎么就攤上這事漱贱。” “怎么了夭委?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵幅狮,是天一觀的道長。 經(jīng)常有香客問我株灸,道長崇摄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任慌烧,我火速辦了婚禮逐抑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屹蚊。我一直安慰自己厕氨,他們只是感情好进每,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著命斧,像睡著了一般田晚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上国葬,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天贤徒,我揣著相機與錄音,去河邊找鬼汇四。 笑死接奈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的船殉。 我是一名探鬼主播鲫趁,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼利虫!你這毒婦竟也來了挨厚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤糠惫,失蹤者是張志新(化名)和其女友劉穎疫剃,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硼讽,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡巢价,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了固阁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壤躲。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖备燃,靈堂內(nèi)的尸體忽然破棺而出碉克,到底是詐尸還是另有隱情,我是刑警寧澤并齐,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布漏麦,位于F島的核電站,受9級特大地震影響况褪,放射性物質(zhì)發(fā)生泄漏撕贞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一测垛、第九天 我趴在偏房一處隱蔽的房頂上張望捏膨。 院中可真熱鬧,春花似錦食侮、人聲如沸号涯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诚隙。三九已至,卻和暖如春起胰,著一層夾襖步出監(jiān)牢的瞬間久又,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工效五, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留地消,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓畏妖,卻偏偏與公主長得像脉执,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子戒劫,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容