JavaScript 模塊化模式

2018-08-02 16:25 更新

模塊化模式

模塊

模塊是任何健壯的應(yīng)用程序體系結(jié)構(gòu)不可或缺的一部分,特點(diǎn)是有助于保持應(yīng)用項(xiàng)目的代碼單元既能清晰地分離又有組織。

在JavaScript中,實(shí)現(xiàn)模塊有幾個(gè)選項(xiàng),他們包括:

  • 模塊化模式
  • 對象表示法
  • AMD模塊
  • CommonJS 模塊
  • ECMAScript Harmony 模塊

我們在書中后面的現(xiàn)代模塊化JavaScript設(shè)計(jì)模式章節(jié)中將探討這些選項(xiàng)中的最后三個(gè)。

模塊化模式是基于對象的文字部分,所以首先對于更新我們對它們的知識是很有意義的。

對象字面值

在對象字面值的標(biāo)記里,一個(gè)對象被描述為一組以逗號分隔的名稱/值對括在大括號({})的集合。對象內(nèi)部的名稱可以是字符串或是標(biāo)記符后跟著一個(gè)冒號":"。在對象里最后一個(gè)名稱/值對不應(yīng)該以","為結(jié)束符,因?yàn)檫@樣會(huì)導(dǎo)致錯(cuò)誤。

var myObjectLiteral = {

    variableKey: variableValue,

    functionKey: function () {
      // ...
    };
};

對象字面值不要求使用新的操作實(shí)例,但是不能夠在結(jié)構(gòu)體開始使用,因?yàn)榇蜷_"{"可能被解釋為一個(gè)塊的開始。在對象外新的成員會(huì)被加載,使用分配如下:smyModule.property = "someValue"; 下面我們可以看到一個(gè)更完整的使用對象字面值定義一個(gè)模塊的例子:

var myModule = {

  myProperty: "someValue",

  // 對象字面值包含了屬性和方法(properties and methods).
  // 例如,我們可以定義一個(gè)模塊配置進(jìn)對象:
  myConfig: {
    useCaching: true,
    language: "en"
  },

  // 非?;镜姆椒?  myMethod: function () {
    console.log( "Where in the world is Paul Irish today?" );
  },

  // 輸出基于當(dāng)前配置(<span>configuration</span>)的一個(gè)值
  myMethod2: function () {
    console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" );
  },

  // 重寫當(dāng)前的配置(configuration)
  myMethod3: function( newConfig ) {

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

// 輸出: Where in the world is Paul Irish today?
myModule.myMethod();

// 輸出: enabled
myModule.myMethod2();

// 輸出: fr
myModule.myMethod3({
  language: "fr",
  useCaching: false
});

使用對象字面值可以協(xié)助封裝和組織你的代碼。如果你想近一步了解對象字面值可以閱讀 Rebecca Murphey 寫過的關(guān)于此類話題的更深入的文章(depth)

也就是說,如果我們選擇了這種技術(shù),我們可能對模塊模式有同樣的興趣。即使使用對象字面值,但也只有一個(gè)函數(shù)的返回值。

模塊化模式

模塊化模式最初被定義為一種對傳統(tǒng)軟件工程中的類提供私有和公共封裝的方法。

在JavaScript中,模塊化模式用來進(jìn)一步模擬類的概念,通過這樣一種方式:我們可以在一個(gè)單一的對象中包含公共/私有的方法和變量,從而從全局范圍中屏蔽特定的部分。這個(gè)結(jié)果是可以減少我們的函數(shù)名稱與在頁面中其他腳本區(qū)域定義的函數(shù)名稱沖突的可能性。

私有信息

模塊模式使用閉包的方式來將"私有信息",狀態(tài)和組織結(jié)構(gòu)封裝起來。提供了一種將公有和私有方法,變量封裝混合在一起的方式,這種方式防止內(nèi)部信息泄露到全局中,從而避免了和其它開發(fā)者接口發(fā)生沖圖的可能性。在這種模式下只有公有的API 會(huì)返回,其它將全部保留在閉包的私有空間中。

這種方法提供了一個(gè)比較清晰的解決方案,在只暴露一個(gè)接口供其它部分使用的情況下,將執(zhí)行繁重任務(wù)的邏輯保護(hù)起來。這個(gè)模式非常類似于立即調(diào)用函數(shù)式表達(dá)式(IIFE-查看命名空間相關(guān)章節(jié)獲取更多信息),但是這種模式返回的是對象,而立即調(diào)用函數(shù)表達(dá)式返回的是一個(gè)函數(shù)。

需要注意的是,在javascript事實(shí)上沒有一個(gè)顯式的真正意義上的"私有性"概念,因?yàn)榕c傳統(tǒng)語言不同,javascript沒有訪問修飾符。從技術(shù)上講,變量不能被聲明為公有的或者私有的,因此我們使用函數(shù)域的方式去模擬這個(gè)概念。在模塊模式中,因?yàn)殚]包的緣故,聲明的變量或者方法只在模塊內(nèi)部有效。在返回對象中定義的變量或者方法可以供任何人使用。

歷史

從歷史角度來看,模塊模式最初是在2003年由一群人共同發(fā)展出來的,這其中包括Richard Cornford。后來通過Douglas Crockford的演講,逐漸變得流行起來。另外一件事情是,如果你曾經(jīng)用過雅虎的YUI庫,你會(huì)看到其中的一些特性和模塊模式非常類似,而這種情況的原因是在創(chuàng)建YUI框架的時(shí)候,模塊模式極大的影響了YUI的設(shè)計(jì)。

例子

下面這個(gè)例子通過創(chuàng)建一個(gè)自包含的模塊實(shí)現(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;
    }
  };

})();

// Usage:

// Increment our counter
testModule.incrementCounter();

// Check the counter value and reset
// Outputs: 1
testModule.resetCounter();

在這里我們看到,其它部分的代碼不能直接訪問我們的incrementCounter() 或者 resetCounter()的值。counter變量被完全從全局域中隔離起來了,因此其表現(xiàn)的就像一個(gè)私有變量一樣,它的存在只局限于模塊的閉包內(nèi)部,因此只有兩個(gè)函數(shù)可以訪問counter。我們的方法是有名字空間限制的,因此在我們代碼的測試部分,我們需要給所有函數(shù)調(diào)用前面加上模塊的名字(例如"testModule")。

當(dāng)使用模塊模式時(shí),我們會(huì)發(fā)現(xiàn)通過使用簡單的模板,對于開始使用模塊模式非常有用。下面是一個(gè)模板包含了命名空間,公共變量和私有變量。

var myNamespace = (function () {

  var myPrivateVar, myPrivateMethod;

  // A private counter variable
  myPrivateVar = 0;

  // A private function which logs any arguments
  myPrivateMethod = function( foo ) {
      console.log( foo );
  };

  return {

    // A public variable
    myPublicVar: "foo",

    // A public function utilizing privates
    myPublicFunction: function( bar ) {

      // Increment our private counter
      myPrivateVar++;

      // Call our private method using bar
      myPrivateMethod( bar );

    }
  };

})();

看一下另外一個(gè)例子,下面我們看到一個(gè)使用這種模式實(shí)現(xiàn)的購物車。這個(gè)模塊完全自包含在一個(gè)叫做basketModule 全局變量中。模塊中的購物車數(shù)組是私有的,應(yīng)用的其它部分不能直接讀取。只存在與模塊的閉包中,因此只有可以訪問其域的方法可以訪問這個(gè)變量。

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)部,你可能注意到我們返回了應(yīng)外一個(gè)對象。這個(gè)自動(dòng)賦值給了basketModule 因此我們可以這樣和這個(gè)對象交互。

// basketModule returns an object with a public API we can use

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

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

// Outputs: 2
console.log( basketModule.getItemCount() );

// Outputs: 0.8
console.log( basketModule.getTotal() );

// However, the following will not work:

// Outputs: undefined
// This is because the basket itself is not exposed as a part of our
// the public API
console.log( basketModule.basket );

// This also won't work as it only exists within the scope of our
// basketModule closure, but not the returned public object
console.log( basket );

上面的方法都處于basketModule 的名字空間中。

請注意在上面的basket模塊中 域函數(shù)是如何在我們所有的函數(shù)中被封裝起來的,以及我們?nèi)绾瘟⒓凑{(diào)用這個(gè)域函數(shù),并且將返回值保存下來。這種方式有以下的優(yōu)勢:

  • 可以創(chuàng)建只能被我們模塊訪問的私有函數(shù)。這些函數(shù)沒有暴露出來(只有一些API是暴露出來的),它們被認(rèn)為是完全私有的。
  • 當(dāng)我們在一個(gè)調(diào)試器中,需要發(fā)現(xiàn)哪個(gè)函數(shù)拋出異常的時(shí)候,可以很容易的看到調(diào)用棧,因?yàn)檫@些函數(shù)是正常聲明的并且是命名的函數(shù)。
  • 正如過去 T.J Crowder 指出的,這種模式同樣可以讓我們在不同的情況下返回不同的函數(shù)。我見過有開發(fā)者使用這種技巧用于執(zhí)行UA(尿檢,抽樣檢查)測試,目的是為了在他們的模塊里面針對IE專門提供一條代碼路徑,但是現(xiàn)在我們也可以簡單的使用特征檢測達(dá)到相同的目的。

模塊模式的變體

Import mixins(導(dǎo)入混合)

這個(gè)變體展示了如何將全局(例如 jQuery, Underscore)作為一個(gè)參數(shù)傳入模塊的匿名函數(shù)。這種方式允許我們導(dǎo)入全局,并且按照我們的想法在本地為這些全局起一個(gè)別名。

// Global module
var myModule = (function ( jQ, _ ) {

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

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

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

// Pull in jQuery and Underscore
}( jQuery, _ ));

myModule.publicMethod();

Exports(導(dǎo)出)

這個(gè)變體允許我們聲明全局對象而不用使用它們,同樣也支持在下一個(gè)例子中我們將會(huì)看到的全局導(dǎo)入的概念。

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

    // Module object
  var module = {},
    privateVariable = "Hello World";

  function privateMethod() {
    // ...
  }

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

  return module;

}());

工具箱和框架特定的模塊模式實(shí)現(xiàn)。

Dojo

Dojo提供了一個(gè)方便的方法 dojo.setObject() 來設(shè)置對象。這需要將以"."符號為第一個(gè)參數(shù)的分隔符,如:myObj.parent.child 是指定義在"myOjb"內(nèi)部的一個(gè)對象“parent”,它的一個(gè)屬性為"child"。使用setObject()方法允許我們設(shè)置children 的值,可以創(chuàng)建路徑傳遞過程中的任何對象即使這些它們根本不存在。

例如,如果我們聲明商店命名空間的對象basket.coreas,可以實(shí)現(xiàn)使用傳統(tǒng)的方式如下:

var store = window.store || {};

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

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

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

或使用Dojo1.7(AMD兼容的版本)及以上如下:

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

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

      var basket = [];

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

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

  }()));

});

欲了解更多關(guān)于dojo.setObject()方法的信息,請參閱官方文檔 documentation

ExtJS

對于這些使用Sencha的ExtJS的人們,你們很幸運(yùn),因?yàn)楣俜轿臋n包含一些例子,用于展示如何正確地在框架里面使用模塊模式。

下面我們可以看到一個(gè)例子關(guān)于如何定義一個(gè)名字空間,然后填入一個(gè)包含有私有和公有API的模塊。除了一些語義上的不同之外,這個(gè)例子和使用vanilla javascript 實(shí)現(xiàn)的模塊模式非常相似。

// create namespace
Ext.namespace("myNameSpace");

// create application
myNameSpace.app = function () {

  // do NOT access DOM from here; elements don't exist yet
  // private variables

  var btn1,
      privVar1 = 11;

  // private functions
  var btn1Handler = function ( button, event ) {
      console.log( "privVar1=" + privVar1 );
      console.log( "this.btn1Text=" + this.btn1Text );
    };

  // public space
  return {
    // public properties, e.g. strings to translate
    btn1Text: "Button 1",

    // public methods
    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來實(shí)現(xiàn)模塊模式。下面的例子很大程度上是基于原始由Eric Miraglia實(shí)現(xiàn)的YUI本身的模塊模式,但是和vanillla Javascript 實(shí)現(xiàn)的版本比較起來差異不是很大。

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

    var myPrivateVar, myPrivateMethod;

    // private variables:
    myPrivateVar = "I can be accessed only within Y.store.basket.";

    // private method:
    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." );

            // Within basket, I can access "private" vars and methods:
            Y.log( myPrivateVar );
            Y.log( myPrivateMethod() );

            // The native scope of myPublicMethod is store so we can
            // access public members using "this":
            Y.log( this.myPublicProperty );
        }
    };

})();

jQuery

因?yàn)閖Query編碼規(guī)范沒有規(guī)定插件如何實(shí)現(xiàn)模塊模式,因此有很多種方式可以實(shí)現(xiàn)模塊模式。Ben Cherry 之間提供一種方案,因?yàn)槟K之間可能存在大量的共性,因此通過使用函數(shù)包裝器封裝模塊的定義。

在下面的例子中,定義了一個(gè)library 函數(shù),這個(gè)函數(shù)聲明了一個(gè)新的庫,并且在新的庫(例如 模塊)創(chuàng)建的時(shí)候,自動(dòng)將初始化函數(shù)綁定到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ā)者來講,至少從javascript語言上來講,模塊模式相對于真正的封裝概念更清晰。

其次,模塊模式支持私有數(shù)據(jù)-因此,在模塊模式中,公共部分代碼可以訪問私有數(shù)據(jù),但是在模塊外部,不能訪問類的私有部分(沒開玩笑!感謝David Engfer 的玩笑)。

缺點(diǎn)

模塊模式的缺點(diǎn)是因?yàn)槲覀儾捎貌煌姆绞皆L問公有和私有成員,因此當(dāng)我們想要改變這些成員的可見性的時(shí)候,我們不得不在所有使用這些成員的地方修改代碼。

我們也不能在對象之后添加的方法里面訪問這些私有變量。也就是說,很多情況下,模塊模式很有用,并且當(dāng)使用正確的時(shí)候,潛在地可以改善我們代碼的結(jié)構(gòu)。

其它缺點(diǎn)包括不能為私有成員創(chuàng)建自動(dòng)化的單元測試,以及在緊急修復(fù)bug時(shí)所帶來的額外的復(fù)雜性。根本沒有可能可以對私有成員打補(bǔ)丁。相反地,我們必須覆蓋所有的使用存在bug私有成員的公共方法。開發(fā)者不能簡單的擴(kuò)展私有成員,因此我們需要記得,私有成員并非它們表面上看上去那么具有擴(kuò)展性。

想要了解更深入的信息,可以閱讀 Ben Cherry 這篇精彩的文章。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號