JavaScript 命名空間模式

2018-02-24 15:18 更新

命名空間模式

在這一節(jié)中,我們將探索JavaScript中關(guān)于命名空間的模式。命名空間可被看作位于一個(gè)唯一標(biāo)識(shí)符下的代碼單元的邏輯組合。標(biāo)識(shí)符可以被很多命名空間引用,每一個(gè)命名空間本身可以包含一個(gè)分支的嵌套命名空間(或子命名空間)。

在應(yīng)用開發(fā)過程中,出于很多原因,我們都要使用命名空間。在JavaScript中,它們幫助我們避免在全局空間中于其他對(duì)象或者變量出現(xiàn)沖突。它們對(duì)于在代碼庫中組織功能塊也非常有用,這樣使用代碼就更容易被使用。

將任何重要的腳本或者應(yīng)用納入命名空間是非常重要的,因?yàn)檫@是我們代碼的一層重要保護(hù),使其免于與頁面中使用相同變量或方法名的其它腳本發(fā)生沖突?,F(xiàn)在由于許多第三方標(biāo)記規(guī)律的插入頁面,這可能是我們?cè)诼殬I(yè)生涯的某個(gè)時(shí)刻都需要處理的一個(gè)普遍的問題。作為一個(gè)行為端正的全局命名空間的“公民”,同樣重要的是,因?yàn)橥瑯拥膯栴},我們最好不要阻礙其他開發(fā)人員的腳本運(yùn)行。

雖然JavaScript并沒有像其它語言一樣真正內(nèi)置的支持名稱空間,它具有對(duì)象和閉包,也可以用來達(dá)到相似的效果。

命名空間原理

幾乎所有重要的 Javascript 程序中都會(huì)用到命名空間。除非我們只是編寫簡單的代碼,否則盡力確保正確地實(shí)現(xiàn)命名空間是很有必要的。這也能避免自己的代碼收到第三方代碼的污染。本小節(jié)將闡述以下設(shè)計(jì)模式:

  1. 單一全局變量
  2. 對(duì)象序列化的表示
  3. 內(nèi)嵌的命名空間
  4. 即時(shí)調(diào)用的函數(shù)表達(dá)式
  5. 命名空間注入

單一全局變量

在 JavaScript 中實(shí)現(xiàn)命名空間的一個(gè)流行模式是,選擇一個(gè)全局變量作為引用的主對(duì)象。下面顯示的是此方法的框架實(shí)現(xiàn),示例代碼中返回一個(gè)包含函數(shù)和屬性的對(duì)象:

var myApplication =  (function () {
        function(){
            //...
        },
        return{
            //...
        }
})();

雖然這段代碼能在特定的環(huán)境下運(yùn)行,單一全局變量模式的最大挑戰(zhàn)是如何確保同一頁面中的其他代碼不會(huì)使用相同的全局變量名稱。

前綴命名空間

一個(gè)解決上面所述問題的方法,正如Peter Michaux提到的, 是使用前綴命名空間. 它本質(zhì)上是一個(gè)簡單的概念,但原理是,我們選擇一個(gè)我們想用的(這個(gè)例子中我們用的是myApplication_)唯一的前綴命名空間,然后在這個(gè)前綴的后面定義任意的方法,變量或者其他對(duì)象,就像下面一樣:

var myApplication_propertyA = {};
var myApplication_propertyB = {};
function myApplication_myMethod(){
  //...
}

從減少全局變量的角度來講這是非常有效的,但請(qǐng)記住,使用一個(gè)具有唯一命名的對(duì)象也能達(dá)到同樣的效果。

另一方面,這種模式的最大問題在于,一旦我們的應(yīng)用開始增長,它會(huì)產(chǎn)生大量的全局對(duì)象。全局區(qū)域中對(duì)于我們沒有被其他開發(fā)人員使用的前綴也存在嚴(yán)重的依賴,所以當(dāng)你選擇使用的時(shí)候,一定要小心。

對(duì)象文字表示

對(duì)象文字表示(我們?cè)诒緯哪K模式一節(jié)中也提到過)可被認(rèn)為是一個(gè)對(duì)象包含了一個(gè)集合,這個(gè)集合中存儲(chǔ)的是鍵值對(duì),它們使用分號(hào)將每個(gè)鍵值對(duì)的鍵和值分隔開,這樣這些鍵也可以表示新的命名空間。

var myApplication = {

    // As we've seen, we can easily define functionality for
    // this object literal..
    getInfo:function(){
      //...
    },

    // but we can also populate it to support
    // further object namespaces containing anything
    // anything we wish:
    models : {},
    views : {
        pages : {}
    },
    collections : {}
};

你也可以直接給命名空間添加屬性:

myApplication.foo = function(){
    return "bar";
}

myApplication.utils = {
    toString:function(){
        //...
    },
    export: function(){
        //...
    }
}

對(duì)象文字具有在不污染全局命名空間的情況下幫助組織代碼和參數(shù)的優(yōu)點(diǎn)。如果我們希望創(chuàng)建易讀的可以支持深度嵌套的結(jié)構(gòu),這將非常有用。與簡單的全局變量不同,對(duì)象文字也經(jīng)??紤]測(cè)試相同名字的變量的存在,這樣就極大的降低了沖突的可能性。

下面例子中,我們展示了幾種方法,它們檢查是否變量(對(duì)象或者插件命名空間)存在,如果不存在就定義該變量。

// This doesn't check for existence of "myApplication" in
// the global namespace. Bad practice as we can easily
// clobber an existing variable/namespace with the same name
var myApplication = {};

// The following options *do* check for variable/namespace existence.
// If already defined, we use that instance, otherwise we assign a new
// object literal to myApplication.
//
// Option 1: var myApplication = myApplication || {};
// Option 2  if( !MyApplication ){ MyApplication = {} };
// Option 3: window.myApplication || ( window.myApplication = {} );
// Option 4: var myApplication = $.fn.myApplication = function() {};
// Option 5: var myApplication = myApplication === undefined ? {} : myApplication;

我們經(jīng)常看到開發(fā)人員使用Option1或者Option2,它們都很容易理解,而且他們的結(jié)果也是一樣的。 Option 3 假定我們?cè)谌置臻g中,但也可以寫成下面的方式:

myApplication || (myApplication = {});

這種改變假定myApplication已經(jīng)被初始化,所以它只對(duì)參數(shù)有效,如下:

function foo() {
  myApplication || ( myApplication = {} );
}

// myApplication hasn't been initialized,
// so foo() throws a ReferenceError

foo();

// However accepting myApplication as an
// argument

function foo( myApplication ) {
  myApplication || ( myApplication = {} );
}

foo();

// Even if myApplication === undefined, there is no error
// and myApplication gets set to {} correctly

Options 4 對(duì)于寫jQuery插件很有效:

// If we were to define a new plugin..
var myPlugin = $.fn.myPlugin = function() { ... };

// Then later rather than having to type:
$.fn.myPlugin.defaults = {};

// We can do:
myPlugin.defaults = {};

這樣的結(jié)果是代碼壓縮(最小化)效果好,而且可以節(jié)省查找范圍。

Option 5 跟Option 4有些類似,但它是一個(gè)較長的形式,它用內(nèi)聯(lián)的方式驗(yàn)證myApplication是否未定義,如果未定義就將它定義為一個(gè)對(duì)象,否則就把已經(jīng)定義的值賦給myApplication。

Option 5的展示是為了完整透徹起見,但在大多數(shù)情況下Option 1-4就足夠滿足大多數(shù)需求了。

當(dāng)然,在使用對(duì)象文字實(shí)習(xí)組織代碼結(jié)構(gòu)方面有很多變體. 對(duì)于希望為一個(gè)內(nèi)部封閉的模塊暴漏一個(gè)嵌套的API的小應(yīng)用來說,我們會(huì)發(fā)現(xiàn)自己使用“展示模塊模式”, 這個(gè)模式之前在本書中講過:

var namespace = (function () {

    // defined within the local scope
    var privateMethod1 = function () { /* ... */ },
        privateMethod2 = function () { /* ... */ }
        privateProperty1 = "foobar";

    return {

        // the object literal returned here can have as many
        // nested depths as we wish, however as mentioned,
        // this way of doing things works best for smaller,
        // limited-scope applications in my personal opinion
        publicMethod1: privateMethod1,

        // nested namespace with public properties
        properties:{
            publicProperty1: privateProperty1
        },

        // another tested namespace
        utils:{
            publicMethod2: privateMethod2
        }
        ...
    }
})();

對(duì)象文字的好處就是他們?yōu)槲覀兲峁┝艘环N非常優(yōu)雅的Key/Value語法,使用它,我們可以很容易的封裝我們應(yīng)用中任意獨(dú)特的邏輯,而且能夠清楚的將它與其他代碼區(qū)分開,同時(shí)它為代碼擴(kuò)展提供了堅(jiān)實(shí)的基礎(chǔ)。

一個(gè)可能的弊端就是,對(duì)象文字可能會(huì)導(dǎo)致很長的語法結(jié)構(gòu),你可以選擇利用嵌套命名空間模式(它也使用了同樣的模式作為基礎(chǔ))

這種模式也有很多有用的應(yīng)用。除了命名空間,它也被用來把應(yīng)用的默認(rèn)配置縮減到一個(gè)獨(dú)立的區(qū)域中,這樣一來就修改配置就不需要查遍整個(gè)代碼庫了,對(duì)象文字在這方面表現(xiàn)非常好。下面的例子是一個(gè)假想的配置:

var myConfig = {

    language: "english",

    defaults: {
        enableGeolocation: true,
        enableSharing: false,
        maxPhotos: 20
    },

    theme: {
        skin: "a",
        toolbars: {
            index: "ui-navigation-toolbar",
            pages: "ui-custom-toolbar"   
        }
    }

}

注意:JSON是對(duì)象文字表示的一個(gè)子集,它與上面的例子(比如:JSON的鍵必須是字符串)只有細(xì)微的語法差異。如果出于某種原因,有人想使用JSON來存儲(chǔ)配置信息(比如:當(dāng)發(fā)送到前端的時(shí)候),也是可以的。想了解更多關(guān)于對(duì)象文字表示模式,我建議閱讀Rebecca Murphey 的優(yōu)秀文章 ,她講到了很多我們上面沒有提到的問題。

嵌套命名空間

文字對(duì)象表示的一個(gè)擴(kuò)展就是嵌套命名空間.它也是一個(gè)常用的模式,它降低了代碼沖突的可能,即使某個(gè)命名空間已經(jīng)存在,它嵌套的命名空間沖突的可能性卻很小。

下面的代碼看起來熟悉嗎?

YAHOO.util.Dom.getElementsByClassName("test");

Yahoo!'s YUI 庫經(jīng)常使用嵌套命名空間模式, 當(dāng)我在AOL當(dāng)工程師的時(shí)候,我們?cè)诤芏啻笮蛻?yīng)用中也使用過這種模式。下面是嵌套命名空間的一個(gè)簡單的實(shí)現(xiàn):

var myApp =  myApp || {};

// perform a similar existence check when defining nested
// children
myApp.routers = myApp.routers || {};
myApp.model = myApp.model || {};
myApp.model.special = myApp.model.special || {};

// nested namespaces can be as complex as required:
// myApp.utilities.charting.html5.plotGraph(/*..*/);
// myApp.modules.financePlanner.getSummary();
// myApp.services.social.facebook.realtimeStream.getLatest();

注意: 上面的代碼與YUI3實(shí)現(xiàn)命名空間是不同的。上面的模塊使用沙盒API來保存對(duì)象,而且使用了更少、更短的命名空間。

我們也可以像下面一樣,選擇使用索引屬性來定義新的嵌套命名空間/屬性:

myApp["routers"] = myApp["routers"] || {};
myApp["models"] = myApp["models"] || {};
myApp["controllers"] = myApp["controllers"] || {};

兩種選擇可讀性都很強(qiáng),而且很有條理,它們都提供了與我們可能在其他語言中使用的類似的一種相對(duì)安全的方式來給我們的應(yīng)用添加命名空間.唯一需要注意的是,這需要我們?yōu)g覽器中的JavaScript引擎首先定位到myApp對(duì)象,然后深入挖掘,直到找到我們想使用的方法為止。

這就以為著在查找方面會(huì)增加很多工作,然后開發(fā)人員比如Juriy Zaytsev 以前就做過測(cè)試,而且發(fā)現(xiàn)單個(gè)對(duì)象命名空間與嵌套命名空間在性能方面的差異是可以忽略不計(jì)的。

即時(shí)調(diào)用的函數(shù)表達(dá)式(IIFE)s

早在本書中,我們就簡單的介紹過IIFE (即時(shí)調(diào)用的函數(shù)表達(dá)式) ,它是一個(gè)未命名的函數(shù),在它被定義之后就會(huì)立即執(zhí)行。如果聽起來覺得耳熟,是因?yàn)槟阋郧坝龅竭^并將它稱之為自動(dòng)生效的(或者自動(dòng)調(diào)用的)匿名函數(shù),然而我個(gè)人更認(rèn)為 Ben Alman的 IIFE 命名更準(zhǔn)確。在JavaScript中,因?yàn)樵谝粋€(gè)作用域中顯示定義的變量和函數(shù)只能在作用域中可見,函數(shù)調(diào)用為實(shí)現(xiàn)隱私提供了簡單的方式。

IIFEs 將應(yīng)用邏輯封裝從而將它在全局命名空間中保護(hù)起來,但可以在命名空間范圍內(nèi)使用。

下面是IIFEs的例子:

// an (anonymous) immediately-invoked function expression
(function () { /*...*/})();

// a named immediately-invoked function expression
(function foobar () { /*..*/}());

// this is technically a self-executing function which is quite different
function foobar () { foobar(); }

對(duì)于第一個(gè)例子稍微進(jìn)行一下擴(kuò)展:

var namespace = namespace || {};

// here a namespace object is passed as a function
// parameter, where we assign public methods and
// properties to it
(function( o ){   
    o.foo = "foo";
    o.bar = function(){
        return "bar";   
    };
})( namespace );

console.log( namespace );

雖然可讀,這個(gè)例子可以被更大范圍的擴(kuò)展到說明通用的開發(fā)問題,例如定義隱私的級(jí)別(public/private函數(shù)和變量),以及方便的命名空間擴(kuò)展。我們來瀏覽更多的代碼:

// namespace (our namespace name) and undefined are passed here
// to ensure 1\. namespace can be modified locally and isn't
// overwritten outside of our function context
// 2\. the value of undefined is guaranteed as being truly
// undefined. This is to avoid issues with undefined being
// mutable pre-ES5.

;(function ( namespace, undefined ) {

    // private properties
    var foo = "foo",
        bar = "bar";

    // public methods and properties
    namespace.foobar = "foobar";
    namespace.sayHello = function () {
        speak( "hello world" );
    };

    // private method
    function speak(msg) {
        console.log( "You said: " + msg );
    };

    // check to evaluate whether "namespace" exists in the
    // global namespace - if not, assign window.namespace an
    // object literal

}( window.namespace = window.namespace || {} ));

// we can then test our properties and methods as follows

// public

// Outputs: foobar
console.log( namespace.foobar );

// Outputs: hello world
namescpace.sayHello();

// assigning new properties
namespace.foobar2 = "foobar";

// Outputs: foobar
console.log( namespace.foobar2 );

對(duì)任何可擴(kuò)展的命名空間模式,可擴(kuò)展性當(dāng)然是關(guān)鍵,可以通過使用IIFEs很容易的達(dá)到這個(gè)目標(biāo)。在下面的例子中,我們的"namespace"再次被當(dāng)作參數(shù)傳遞給匿名函數(shù),之后擴(kuò)展(或裝飾)了更多的功能:

// let's extend the namespace with new functionality
(function( namespace, undefined ){

    // public method
    namespace.sayGoodbye = function () {
        console.log( namespace.foo );
        console.log( namespace.bar );
        speak( "goodbye" );
    }   
}( window.namespace = window.namespace || {});

// Outputs: goodbye
namespace.sayGoodbye();

命名空間注入

命名空間注入是關(guān)于IIFE的另外一種變種,為了一個(gè)來自函數(shù)封裝中使用this作為命名空間代理的特定的命名空間,我們將方法和屬性“注入”, 這一模式提供的好處就是對(duì)于多個(gè)對(duì)象或者命名空間的應(yīng)用程序的功能性行為的便利性,并且在應(yīng)用一堆晚些時(shí)候?qū)⒈粯?gòu)建的基礎(chǔ)方法(如getter和setter),這將會(huì)變得很有用處。

這一模式的缺點(diǎn)就是,如我在本節(jié)前面所述,也許還會(huì)有達(dá)成此目的更加簡單并且更加優(yōu)化的方法存在(如,深度對(duì)象擴(kuò)展/混合)。

下面我們馬上可以看到這一模式的一個(gè)示例,我們使用它來填充兩個(gè)命名空間的行為:一個(gè)最開始就定義(utils),而另外一個(gè)我們則將其作為utils的功能性賦值的一部分來動(dòng)態(tài)創(chuàng)建(一個(gè)稱作tools的新的命名空間)。

var myApp = myApp || {};
myApp.utils =  {};

(function () {
  var val = 5;

  this.getValue = function () {
      return val;
  };

  this.setValue = function( newVal ) {
      val = newVal;
  }

  // also introduce a new sub-namespace
  this.tools = {};

}).apply( myApp.utils ); 

// inject new behaviour into the tools namespace
// which we defined via the utilities module

(function () {
    this.diagnose = function(){
        return "diagnosis";  
    }
}).apply( myApp.utils.tools );

// note, this same approach to extension could be applied
// to a regular IIFE, by just passing in the context as
// an argument and modifying the context rather than just
// "this"

// Usage:

// Outputs our populated namespace
console.log( myApp );

// Outputs: 5
console.log( myApp.utils.getValue() );

// Sets the value of `val` and returns it
myApp.utils.setValue( 25 );
console.log( myApp.utils.getValue() );

// Testing another level down
console.log( myApp.utils.tools.diagnose() );

Angus Croll先前也出過使用調(diào)用API來提供上下文環(huán)境和參數(shù)之間自然分離的主意。這一模式感覺上像是一個(gè)模塊創(chuàng)建器,但是由于模塊仍然提供了一個(gè)封裝的解決方案, 為全面起見,我們還是將簡要的介紹一下它:

// define a namespace we can use later
var ns = ns || {},
    ns2 = ns2 || {};

// the module/namespace creator
var creator = function( val ){

    var val = val || 0;

    this.next = function () {
        return val++
    };

    this.reset = function () {
        val = 0;
    }
}

creator.call( ns );

// ns.next, ns.reset now exist
creator.call( ns2 , 5000 );

// ns2 contains the same methods
// but has an overridden value for val
// of 5000

如前所述,這種類型的模式對(duì)于將一個(gè)類似的功能的基礎(chǔ)集合分派給多個(gè)模塊或者命名空間是非常有用的。然而我會(huì)只建議將它使用在要在一個(gè)對(duì)象/閉包中明確聲明功能,而直接訪問并沒有任何意義的地方。

高級(jí)命名空間模式

接下來說說我在開發(fā)大型應(yīng)用過程中發(fā)現(xiàn)的幾種有用的模式和工具,其中一些需要我們重新審視傳統(tǒng)應(yīng)用的命名空間的使用方式.需要注意的是,我并非有意夸大以下幾種是正確的命名空間之路,只是我在工作中發(fā)現(xiàn)他們確實(shí)好用。

自動(dòng)嵌套命名空間

我們提到過,嵌套命名空間可以為代碼提供一個(gè)組織良好的層級(jí)結(jié)構(gòu).下邊是一個(gè)例子:application.utilities.drawing.canvas.2d . 可以用文字對(duì)象模式展開如下:

var application = {
      utilities:{
          drawing:{
              canvas:{
                  2d:{
                          //...
                  }
              }
          }
    }       
};

使用這種模式會(huì)遇到一些問題,一個(gè)顯而易見的就是每天加一個(gè)層級(jí),就需要我們?cè)陧敿?jí)命名空間下的某個(gè)父級(jí)元素里定義一個(gè)額外的對(duì)象.當(dāng)應(yīng)用越來越復(fù)雜的時(shí)候,我們需要的層級(jí)增多,解決這個(gè)問題也就更加困難。

怎樣更好的解決這個(gè)問題呢? 在JavaScript設(shè)計(jì)模式中, Stoyan Stefanov 提出了一個(gè)非常精巧的方法以便在已存在的全局變量下定義嵌套的命名空間。 他建議的簡便方法是為每一層嵌套提供一個(gè)單字符聲明,解析這個(gè)聲明就可以自動(dòng)算出包含必要對(duì)象的命名空間。

我(筆者)將他建議使用的方法改進(jìn)為一個(gè)通用方法,以便對(duì)多重命名空間更容易地做出復(fù)用,方法如下:

// 頂級(jí)命名空間賦值為對(duì)象字面量
var myApp = myApp || {};

// 解析字符命名空間并自動(dòng)生成嵌套命名空間的快捷方法 function extend( ns, ns_string ) {
    var parts = ns_string.split("."),
        parent = ns,
        pl;

    pl = parts.length;

    for ( var i = 0; i < pl; i++ ) {
        // create a property if it doesn't exist
        if ( typeof parent[parts[i]] === "undefined" ) {
            parent[parts[i]] = {};
        }

        parent = parent[parts[i]];
    }

    return parent;
}

// 用法:
// extend為myApp加入深度嵌套的命名空間
var mod = extend(myApp, "modules.module2");

// 輸出深度嵌套的正確對(duì)象
console.log(mod);

// 用于檢查mod的實(shí)例作為包含擴(kuò)展的一個(gè)實(shí)體也能夠被myApp命名空間以外被使用的少量測(cè)試

// 輸出: true
console.log(mod == myApp.modules.module2);

// 進(jìn)一步演示用extend賦予嵌套命名空間更簡單
extend(myApp, "moduleA.moduleB.moduleC.moduleD");
extend(myApp, "longer.version.looks.like.this");
console.log(myApp);

Web審查工具輸出:

一行簡潔的代碼就可以很輕松地,為他們的命名空間像以前的對(duì)象那樣明確聲明各種各樣的嵌套。

依賴聲明模式

現(xiàn)在我們將探索一種對(duì)嵌套命名空間模式的一種輕微的增強(qiáng),它將被我們引申為依賴聲明模式。我們都知道對(duì)于對(duì)象的本地引用能夠降低全局查找的時(shí)間,但讓我們來將它應(yīng)用在命名空間中,看看實(shí)踐中它表現(xiàn)怎么樣:

// common approach to accessing nested namespaces
myApp.utilities.math.fibonacci( 25 );
myApp.utilities.math.sin( 56 );
myApp.utilities.drawing.plot( 98,50,60 );

// with local/cached references
var utils = myApp.utilities,
maths = utils.math,
drawing = utils.drawing;

// easier to access the namespace
maths.fibonacci( 25 );
maths.sin( 56 );
drawing.plot( 98, 50,60 );

// note that the above is particularly performant when
// compared to hundreds or thousands of calls to nested
// namespaces vs. a local reference to the namespace

這里使用一個(gè)本地變量相比頂層上一個(gè)全局的(如,myApp)幾乎總是會(huì)更快。相比訪問其后每行嵌套的屬性/命名空間,這也更加的方便,性能表現(xiàn)更好,并且能夠在更加復(fù)雜的應(yīng)用程序場(chǎng)景下面提升可讀性。

Stoyan建議在我們的函數(shù)范圍(使用單變量模式)的頂部聲明函數(shù)或者模塊需要的局部命名空間,并把這稱為依賴聲明模式。其中的一個(gè)好處是減少了定位和重定向依賴關(guān)系的時(shí)間,從而使我們有一個(gè)可擴(kuò)展的架構(gòu),當(dāng)需要時(shí)可以在命名空間里動(dòng)態(tài)地加載模塊。

在我看來,這種方式應(yīng)用于模塊化級(jí)別時(shí),將被其他方法使用的命名空間局部化是最有效。我建議盡量避免把命名空間局部化在單個(gè)函數(shù)級(jí)別,尤其是對(duì)于命名空間的依賴關(guān)系上有明顯的重疊的情況。對(duì)應(yīng)的方法是,在上部定義并使它們可以進(jìn)入同一個(gè)引用。

深度對(duì)象擴(kuò)展

另一種實(shí)現(xiàn)自動(dòng)命名空間的方式就是深度對(duì)象擴(kuò)展. 使用對(duì)象文字表示的命名空間可以很容易地與其他對(duì)象(或命名空間)擴(kuò)展(或者合并) 這樣兩個(gè)命名空間下的屬性和方法就可以在同一個(gè)合并后的命名空間下被訪問。

一些現(xiàn)代的JavaScript框架已經(jīng)把這個(gè)變得非常容易(例如,jQuery的$.extend),然而,如果你想尋找一種使用普通的JS來擴(kuò)展對(duì)象(命名空間)的方式,下面的內(nèi)容將很有幫助。

// extend.js
// Written by Andrew Dupont, optimized by Addy Osmani

function extend( destination, source ) {

    var toString = Object.prototype.toString,
        objTest = toString.call({});

    for ( var property in source ) {
        if ( source[property] && objTest === toString.call(source[property]) ) {
            destination[property] = destination[property] || {};
            extend(destination[property], source[property]);
        } else {
            destination[property] = source[property];
        }
    }
    return destination;

};

console.group( "objExtend namespacing tests" );

// define a top-level namespace for usage
var myNS = myNS || {};

// 1\. extend namespace with a "utils" object
extend(myNS, {
        utils:{
        }
});

console.log( "test 1" , myNS);
// myNS.utils now exists

// 2\. extend with multiple depths (namespace.hello.world.wave)
extend(myNS, {
                hello:{
                        world:{
                                wave:{
                                    test: function(){
                                        //...
                                    }
                                }
                        }
                }
});

// test direct assignment works as expected
myNS.hello.test1 = "this is a test";
myNS.hello.world.test2 = "this is another test";
console.log( "test 2", myNS );

// 3\. what if myNS already contains the namespace being added
// (e.g. "library")? we want to ensure no namespaces are being
// overwritten during extension

myNS.library = {
        foo:function () {}
};

extend( myNS, {
        library:{
                bar:function(){
                    //...
                }
        }
});

// confirmed that extend is operating safely (as expected)
// myNS now also contains library.foo, library.bar
console.log( "test 3", myNS );

// 4\. what if we wanted easier access to a specific namespace without having
// to type the whole namespace out each time?

var shorterAccess1 = myNS.hello.world;
shorterAccess1.test3 = "hello again";
console.log( "test 4", myNS);

//success, myApp.hello.world.test3 is now "hello again"

console.groupEnd();

注意: 上面的實(shí)現(xiàn)對(duì)于所有的對(duì)象來說不是跨瀏覽器的而且只應(yīng)該被認(rèn)為是一個(gè)概念上的證明. 你可能會(huì)覺得前面帶下劃線的js.extend()方法更簡單一些,下面的鏈接提供了更多的跨瀏覽器實(shí)現(xiàn),http://documentcloud.github.com/underscore/docs/underscore.html#section-67。 另外,從代碼中抽取出來的jQuery $.extend() 方法可以在這里找到:?https://github.com/addyosmani/jquery.parts。 對(duì)于那些將使用jQuery的開發(fā)者來說, 可以像下面一樣使用$.extend來達(dá)到同樣的對(duì)象命名空間擴(kuò)展的目的:

// top-level namespace
var myApp = myApp || {};

// directly assign a nested namespace
myApp.library = {
  foo:function(){
    //...
  }
};

// deep extend/merge this namespace with another
// to make things interesting, let's say it's a namespace
// with the same name but with a different function
// signature: $.extend( deep, target, object1, object2 )
$.extend( true, myApp, {
    library:{
        bar:function(){
            //...
        }
    }
});

console.log("test", myApp);
// myApp now contains both library.foo() and library.bar() methods
// nothing has been overwritten which is what we're hoping for.

為了透徹起見,請(qǐng)點(diǎn)擊這里 來查看jQuery $.extend來獲取跟這一節(jié)中其它實(shí)現(xiàn)命名空間的過程類似的功能。

建議

回顧我們?cè)诒静糠痔接懙拿臻g模式,對(duì)于大多數(shù)更大的應(yīng)用程序,我個(gè)人則是選擇嵌入用對(duì)象字面值模式為命名空間的對(duì)象。我盡可能地用自動(dòng)嵌入命名空間,當(dāng)然這只是個(gè)人偏好罷了。

IIFEs 和單個(gè)全局變量可能只在中小規(guī)模的應(yīng)用程序中運(yùn)轉(zhuǎn)良好。然而,更大的需要命名空間和深度子命名空間的代碼庫則需要一個(gè)簡明的,能提高可讀性和規(guī)模的解決方案。我認(rèn)為是這種模式很好地達(dá)到了這些目標(biāo)。我同樣推薦你嘗試一些拓展命名空間的高級(jí)實(shí)用的方法,因?yàn)樗鼈兡荛L期地節(jié)省我們的時(shí)間。

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)