模塊模式

The Module Pattern

Modules

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

在JS中,有如下幾個關于實現模塊的選項,他們包括

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

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

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

對象字面量

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

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

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

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

var myModule = {

  myProperty: "someValue",

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

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

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

  // 重寫現有配置的方法
  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();

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

The Module Pattern

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

私有

模塊模式利用閉包來封裝私有的、狀態或者組織。它提供一種封裝公有和私有方法的混合方法,保護內部的內容不泄露到全局的作用域中并與其他的開發人員的接口相沖突。在這種模式中,只有公有的方法會被返回,將所有其他的內容都保留在了閉包內。

這給我們提供了一個簡潔的解決方案,屏蔽內部復雜邏輯并僅僅暴露我們的應用其他部分希望使用的接口,該模式和立即被調用的函數表達式非常相似,除了返回一個對象而不是一個函數。

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

History

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

例子

讓我們通過創建一個獨立的模塊來看看如何實現模塊模式


var testModule = (function () {

  var counter = 0;

  return {

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

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

})();

// 用法

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

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

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

當使用模塊模式時,我們可能會發現我們定義一個簡單的模板,對開始使用它是有用的。這有一個包含命名空間,公有/私有變量的例子:


var myNamespace = (function () {

  var myPrivateVar, myPrivateMethod;

  // 一個私有的計數器變量
  myPrivateVar = 0;

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

  return {

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

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

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

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

    }
  };

})();

來看另一個例子,如下我們可以看到一個使用這種模式實現的購物籃。這個模塊是完全獨立的在,在全局變量中被叫做basketModule。其中basket數組在模塊中保持私有狀態,所以我們應用中的其他部分是不可以直接讀取它的。它僅僅存在于模塊的閉包之中,并且在模塊外部能訪問的僅僅是它暴露出的方法,例如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;
    }
  };
})();

你可能會注意到,在模塊內部,我們返回了一個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 );

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

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

注意如何界定上面的basket模塊中包含的功能,即我們之后立即調用并存儲的返回值。這里有一系列的優點如下:

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

Module Pattern Variations

Import mixins(導入混入?)

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


//              此處可以起別名
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導入到匿名函數中
})( 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;

})();

工具包和特定框架的模塊模式實現

Dojo

Dojo提供了一些便利的方法處理對象,通過對象的方法調用dojo.setObject()。用點分割的字符串,作為它的第一個參數,就像myObject.parent.child中一個“child"作為"parent"的一個屬性,而"parent"則定義在"myObj"中。使用 setObject()允許我們去設置它的子對象,如果在給定的路徑中對象不存在的話,創建中間的對象。

例如你想創建一個baseket.core作為store命名空間下的對象,這可以用下面的一般性方式實現。

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兼容版)這個庫并配合如下代碼實現

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

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

      var basket = [];

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

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

  })());

});

如果想了解更多的關于dojo.setObject的信息,請查看官方文檔

ExtJS

關于那些使用了Sencha的ExtJs的,下面的一個例子告訴你,如何利用框架正確的使用模塊模式。

這里,我們看到了一個關于如何定義命名空間,然后利用包含公有和私有的API模塊填充它的例子。除了一些語法不同外,它與VanillaJs中的模塊模式的實現非常接近。


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

// 在命名空間中創建應用
myNameSpace.app = function () {

  // 不要在這里創建引用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

相似的,當我們構建我們的應用程序的時候,也可以使用YUI3實現模塊模式。
接下來的這個例子嚴重的依賴原來的YUI模塊模式,由Eric Miraglia實現,但是,它與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之前建議過一種實現,將一些含有共同點的模塊,通過包裹在函數中定義為一個模塊。

在下面的例子中,一個library方法被定義,它聲明了一個庫,并在它被創建的時候,自動的將它的inti方法綁定到了document.ready方法中。

function library( module ) {

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

  return module;
}

var myLibrary = library(function () {

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

優點

我們已經看到了模塊模式為何是有用的,但是為什么模塊模式是一個好的選擇?對于一個初學者來說,有著面向對象背景的一個開發者來說,相比真正的封裝而言,這樣會更整潔,至少從JS角度來看是這樣。

其實,它支持私有數據,所以在模塊模式中,我們的公有部分的代碼可以接觸到私有的部分,然而模塊外的世界則不可接觸到模塊中的私有部分。

缺點

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

我們也無法在方法中訪問之后被添加到對象的私有成員。也就是說,在許多情況下模塊模式仍是非常有用的,當使用正確,當然有潛力干山我們的應用程序結構。

其他的缺點包括無法為私有成員創建單元測試,當錯誤需要熱修復時,增加了額外的復雜度。修復私有的地方簡直是不可能的。相反,一個人必須修改所有的與有bug的私有成員交互的公有成員,開發者也無法輕易的擴展私有成員,所以值得記住的是私有成員并不像最初呈現的那樣靈活。

關于更多關于模塊模式的內容,可以參考Ben Cherry的優秀的深入的文章。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容