离线下载
PDF版 ePub版

史涛 · 更新于 2018-11-28 11:00:43

命名空间模式

在这一节中,我们将探索JavaScript中关于命名空间的模式。命名空间可被看作位于一个唯一标识符下的代码单元的逻辑组合。标识符可以被很多命名空间引用,每一个命名空间本身可以包含一个分支的嵌套命名空间(或子命名空间)。

在应用开发过程中,出于很多原因,我们都要使用命名空间。在JavaScript中,它们帮助我们避免在全局空间中于其他对象或者变量出现冲突。它们对于在代码库中组织功能块也非常有用,这样使用代码就更容易被使用。

将任何重要的脚本或者应用纳入命名空间是非常重要的,因为这是我们代码的一层重要保护,使其免于与页面中使用相同变量或方法名的其它脚本发生冲突。现在由于许多第三方标记规律的插入页面,这可能是我们在职业生涯的某个时刻都需要处理的一个普遍的问题。作为一个行为端正的全局命名空间的“公民”,同样重要的是,因为同样的问题,我们最好不要阻碍其他开发人员的脚本运行。

虽然JavaScript并没有像其它语言一样真正内置的支持名称空间,它具有对象和闭包,也可以用来达到相似的效果。

命名空间原理

几乎所有重要的 Javascript 程序中都会用到命名空间。除非我们只是编写简单的代码,否则尽力确保正确地实现命名空间是很有必要的。这也能避免自己的代码收到第三方代码的污染。本小节将阐述以下设计模式:

  1. 单一全局变量
  2. 对象序列化的表示
  3. 内嵌的命名空间
  4. 即时调用的函数表达式
  5. 命名空间注入

单一全局变量

在 JavaScript 中实现命名空间的一个流行模式是,选择一个全局变量作为引用的主对象。下面显示的是此方法的框架实现,示例代码中返回一个包含函数和属性的对象:

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

虽然这段代码能在特定的环境下运行,单一全局变量模式的最大挑战是如何确保同一页面中的其他代码不会使用相同的全局变量名称。

前缀命名空间

一个解决上面所述问题的方法,正如Peter Michaux提到的, 是使用前缀命名空间. 它本质上是一个简单的概念,但原理是,我们选择一个我们想用的(这个例子中我们用的是myApplication_)唯一的前缀命名空间,然后在这个前缀的后面定义任意的方法,变量或者其他对象,就像下面一样:

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

从减少全局变量的角度来讲这是非常有效的,但请记住,使用一个具有唯一命名的对象也能达到同样的效果。

另一方面,这种模式的最大问题在于,一旦我们的应用开始增长,它会产生大量的全局对象。全局区域中对于我们没有被其他开发人员使用的前缀也存在严重的依赖,所以当你选择使用的时候,一定要小心。

对象文字表示

对象文字表示(我们在本书的模块模式一节中也提到过)可被认为是一个对象包含了一个集合,这个集合中存储的是键值对,它们使用分号将每个键值对的键和值分隔开,这样这些键也可以表示新的命名空间。

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(){
        //...
    }
}

对象文字具有在不污染全局命名空间的情况下帮助组织代码和参数的优点。如果我们希望创建易读的可以支持深度嵌套的结构,这将非常有用。与简单的全局变量不同,对象文字也经常考虑测试相同名字的变量的存在,这样就极大的降低了冲突的可能性。

下面例子中,我们展示了几种方法,它们检查是否变量(对象或者插件命名空间)存在,如果不存在就定义该变量。

// 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;

我们经常看到开发人员使用Option1或者Option2,它们都很容易理解,而且他们的结果也是一样的。 Option 3 假定我们在全局命名空间中,但也可以写成下面的方式:

myApplication || (myApplication = {});

这种改变假定myApplication已经被初始化,所以它只对参数有效,如下:

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 对于写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 = {};

这样的结果是代码压缩(最小化)效果好,而且可以节省查找范围。

Option 5 跟Option 4有些类似,但它是一个较长的形式,它用内联的方式验证myApplication是否未定义,如果未定义就将它定义为一个对象,否则就把已经定义的值赋给myApplication。

Option 5的展示是为了完整透彻起见,但在大多数情况下Option 1-4就足够满足大多数需求了。

当然,在使用对象文字实习组织代码结构方面有很多变体. 对于希望为一个内部封闭的模块暴漏一个嵌套的API的小应用来说,我们会发现自己使用“展示模块模式”, 这个模式之前在本书中讲过:

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
        }
        ...
    }
})();

对象文字的好处就是他们为我们提供了一种非常优雅的Key/Value语法,使用它,我们可以很容易的封装我们应用中任意独特的逻辑,而且能够清楚的将它与其他代码区分开,同时它为代码扩展提供了坚实的基础。

一个可能的弊端就是,对象文字可能会导致很长的语法结构,你可以选择利用嵌套命名空间模式(它也使用了同样的模式作为基础)

这种模式也有很多有用的应用。除了命名空间,它也被用来把应用的默认配置缩减到一个独立的区域中,这样一来就修改配置就不需要查遍整个代码库了,对象文字在这方面表现非常好。下面的例子是一个假想的配置:

var myConfig = {

    language: "english",

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

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

}

注意:JSON是对象文字表示的一个子集,它与上面的例子(比如:JSON的键必须是字符串)只有细微的语法差异。如果出于某种原因,有人想使用JSON来存储配置信息(比如:当发送到前端的时候),也是可以的。想了解更多关于对象文字表示模式,我建议阅读Rebecca Murphey 的优秀文章 ,她讲到了很多我们上面没有提到的问题。

嵌套命名空间

文字对象表示的一个扩展就是嵌套命名空间.它也是一个常用的模式,它降低了代码冲突的可能,即使某个命名空间已经存在,它嵌套的命名空间冲突的可能性却很小。

下面的代码看起来熟悉吗?

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

Yahoo!'s YUI 库经常使用嵌套命名空间模式, 当我在AOL当工程师的时候,我们在很多大型应用中也使用过这种模式。下面是嵌套命名空间的一个简单的实现:

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实现命名空间是不同的。上面的模块使用沙盒API来保存对象,而且使用了更少、更短的命名空间。

我们也可以像下面一样,选择使用索引属性来定义新的嵌套命名空间/属性:

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

两种选择可读性都很强,而且很有条理,它们都提供了与我们可能在其他语言中使用的类似的一种相对安全的方式来给我们的应用添加命名空间.唯一需要注意的是,这需要我们浏览器中的JavaScript引擎首先定位到myApp对象,然后深入挖掘,直到找到我们想使用的方法为止。

这就以为着在查找方面会增加很多工作,然后开发人员比如Juriy Zaytsev 以前就做过测试,而且发现单个对象命名空间与嵌套命名空间在性能方面的差异是可以忽略不计的。

即时调用的函数表达式(IIFE)s

早在本书中,我们就简单的介绍过IIFE (即时调用的函数表达式) ,它是一个未命名的函数,在它被定义之后就会立即执行。如果听起来觉得耳熟,是因为你以前遇到过并将它称之为自动生效的(或者自动调用的)匿名函数,然而我个人更认为 Ben Alman的 IIFE 命名更准确。在JavaScript中,因为在一个作用域中显示定义的变量和函数只能在作用域中可见,函数调用为实现隐私提供了简单的方式。

IIFEs 将应用逻辑封装从而将它在全局命名空间中保护起来,但可以在命名空间范围内使用。

下面是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(); }

对于第一个例子稍微进行一下扩展:

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 );

虽然可读,这个例子可以被更大范围的扩展到说明通用的开发问题,例如定义隐私的级别(public/private函数和变量),以及方便的命名空间扩展。我们来浏览更多的代码:

// 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 );

对任何可扩展的命名空间模式,可扩展性当然是关键,可以通过使用IIFEs很容易的达到这个目标。在下面的例子中,我们的"namespace"再次被当作参数传递给匿名函数,之后扩展(或装饰)了更多的功能:

// 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();

命名空间注入

命名空间注入是关于IIFE的另外一种变种,为了一个来自函数封装中使用this作为命名空间代理的特定的命名空间,我们将方法和属性“注入”, 这一模式提供的好处就是对于多个对象或者命名空间的应用程序的功能性行为的便利性,并且在应用一堆晚些时候将被构建的基础方法(如getter和setter),这将会变得很有用处。

这一模式的缺点就是,如我在本节前面所述,也许还会有达成此目的更加简单并且更加优化的方法存在(如,深度对象扩展/混合)。

下面我们马上可以看到这一模式的一个示例,我们使用它来填充两个命名空间的行为:一个最开始就定义(utils),而另外一个我们则将其作为utils的功能性赋值的一部分来动态创建(一个称作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先前也出过使用调用API来提供上下文环境和参数之间自然分离的主意。这一模式感觉上像是一个模块创建器,但是由于模块仍然提供了一个封装的解决方案, 为全面起见,我们还是将简要的介绍一下它:

// 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

如前所述,这种类型的模式对于将一个类似的功能的基础集合分派给多个模块或者命名空间是非常有用的。然而我会只建议将它使用在要在一个对象/闭包中明确声明功能,而直接访问并没有任何意义的地方。

高级命名空间模式

接下来说说我在开发大型应用过程中发现的几种有用的模式和工具,其中一些需要我们重新审视传统应用的命名空间的使用方式.需要注意的是,我并非有意夸大以下几种是正确的命名空间之路,只是我在工作中发现他们确实好用。

自动嵌套命名空间

我们提到过,嵌套命名空间可以为代码提供一个组织良好的层级结构.下边是一个例子:application.utilities.drawing.canvas.2d . 可以用文字对象模式展开如下:

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

使用这种模式会遇到一些问题,一个显而易见的就是每天加一个层级,就需要我们在顶级命名空间下的某个父级元素里定义一个额外的对象.当应用越来越复杂的时候,我们需要的层级增多,解决这个问题也就更加困难。

怎样更好的解决这个问题呢? 在JavaScript设计模式中, Stoyan Stefanov 提出了一个非常精巧的方法以便在已存在的全局变量下定义嵌套的命名空间。 他建议的简便方法是为每一层嵌套提供一个单字符声明,解析这个声明就可以自动算出包含必要对象的命名空间。

我(笔者)将他建议使用的方法改进为一个通用方法,以便对多重命名空间更容易地做出复用,方法如下:

// 顶级命名空间赋值为对象字面量
var myApp = myApp || {};

// 解析字符命名空间并自动生成嵌套命名空间的快捷方法 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");

// 输出深度嵌套的正确对象
console.log(mod);

// 用于检查mod的实例作为包含扩展的一个实体也能够被myApp命名空间以外被使用的少量测试

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

// 进一步演示用extend赋予嵌套命名空间更简单
extend(myApp, "moduleA.moduleB.moduleC.moduleD");
extend(myApp, "longer.version.looks.like.this");
console.log(myApp);

Web审查工具输出:

一行简洁的代码就可以很轻松地,为他们的命名空间像以前的对象那样明确声明各种各样的嵌套。

依赖声明模式

现在我们将探索一种对嵌套命名空间模式的一种轻微的增强,它将被我们引申为依赖声明模式。我们都知道对于对象的本地引用能够降低全局查找的时间,但让我们来将它应用在命名空间中,看看实践中它表现怎么样:

// 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

这里使用一个本地变量相比顶层上一个全局的(如,myApp)几乎总是会更快。相比访问其后每行嵌套的属性/命名空间,这也更加的方便,性能表现更好,并且能够在更加复杂的应用程序场景下面提升可读性。

Stoyan建议在我们的函数范围(使用单变量模式)的顶部声明函数或者模块需要的局部命名空间,并把这称为依赖声明模式。其中的一个好处是减少了定位和重定向依赖关系的时间,从而使我们有一个可扩展的架构,当需要时可以在命名空间里动态地加载模块。

在我看来,这种方式应用于模块化级别时,将被其他方法使用的命名空间局部化是最有效。我建议尽量避免把命名空间局部化在单个函数级别,尤其是对于命名空间的依赖关系上有明显的重叠的情况。对应的方法是,在上部定义并使它们可以进入同一个引用。

深度对象扩展

另一种实现自动命名空间的方式就是深度对象扩展. 使用对象文字表示的命名空间可以很容易地与其他对象(或命名空间)扩展(或者合并) 这样两个命名空间下的属性和方法就可以在同一个合并后的命名空间下被访问。

一些现代的JavaScript框架已经把这个变得非常容易(例如,jQuery的$.extend),然而,如果你想寻找一种使用普通的JS来扩展对象(命名空间)的方式,下面的内容将很有帮助。

// 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();

注意: 上面的实现对于所有的对象来说不是跨浏览器的而且只应该被认为是一个概念上的证明. 你可能会觉得前面带下划线的js.extend()方法更简单一些,下面的链接提供了更多的跨浏览器实现,http://documentcloud.github.com/underscore/docs/underscore.html#section-67。 另外,从代码中抽取出来的jQuery $.extend() 方法可以在这里找到: https://github.com/addyosmani/jquery.parts。 对于那些将使用jQuery的开发者来说, 可以像下面一样使用$.extend来达到同样的对象命名空间扩展的目的:

// 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.

为了透彻起见,请点击这里 来查看jQuery $.extend来获取跟这一节中其它实现命名空间的过程类似的功能。

建议

回顾我们在本部分探讨的命名空间模式,对于大多数更大的应用程序,我个人则是选择嵌入用对象字面值模式为命名空间的对象。我尽可能地用自动嵌入命名空间,当然这只是个人偏好罢了。

IIFEs 和单个全局变量可能只在中小规模的应用程序中运转良好。然而,更大的需要命名空间和深度子命名空间的代码库则需要一个简明的,能提高可读性和规模的解决方案。我认为是这种模式很好地达到了这些目标。我同样推荐你尝试一些拓展命名空间的高级实用的方法,因为它们能长期地节省我们的时间。