JS命名空间模式解析

简介

在SF上看到这样一个提问:html

如题,由于不得已的缘由,须要写若干个全局函数。但又不想这样:编程

window.a = function(){}
window.b = function(){}
window.c = function(){}

题主问有什么好的写法?数组

解答:浏览器

若是你用 jQuery,你能够这样写缓存

$.extend(window, {
    a: function() {},
    b: function() {},
    c: function() {}
});

若是你不用 jQuery,能够直接实现相似的 extend:安全

(() => {
    var defining = {
        a: function() { },
        b: function() { },
        c: function() { }
    };

    Object.keys(defining).forEach(key => {
        window[key] = defining[key];
    });
})();

在JavaScript中,命名空间能够帮助咱们防止与全局命名空间下的其余对象或变量产生冲突。命名空间也有助于组织代码,有更强的可维护性和可读性。本文旨在探讨JavaScript里的几种常见命名空间模式,为咱们提供一个思路。闭包

通常命名空间的实现都以window为根。当向window申请a.b.c的命名空间时,首先在window中查看是否存在a这个成员,若是没有则在 window下新建一个名为a的空关联数组,若是已经存在a,则继续在window.a中查看b是否存在,以此类推。下面分别是Atlas和YUI中的实现方法。app

Atlas命名空间的实现方法:函数

Function.registerNamespace =function(namespacePath){
    //以window为根
    var rootObject =window;
    //对命名空间路径拆分红数组
    var namespaceParts =namespacePath.split('.');
    for (var i =0;i <namespaceParts.length;i++) {
        var currentPart =namespaceParts[i];
        //若是当前命名空间下不存在,则新建一个Object对象,等效于一个关联数组。
        if(!rootObject[currentPart])      {
           rootObject[currentPart]=new Object();
        }
        rootObject =rootObject[currentPart];
    }
}

YUI命名空间的实现方法:post

var YAHOO = window.YAHOO || {};
YAHOO.namespace = function(ns) {
    if (!ns || !ns.length) {
        return null;
    }
    var levels = ns.split(".");
    var nsobj = YAHOO;

    //若是申请的命名空间是在YAHOO下的,则必须忽略它,不然就成了YAHOO.YAHOO了
    for (var i=(levels[0] == "YAHOO") ? 1 : 0; i<levels.length; ++i) {

        //若是当前命名空间下不存在,则新建一个关联数组。
        nsobj[levels[i]] = nsobj[levels[i]] || {};
        nsobj = nsobj[levels[i]];
    }

    //返回所申请命名空间的一个引用;
    return nsobj;
};

YUI把全部申请的命名空间都放在了window.YAHOO下面,这样有什么好处呢?假如Yahoo和其余公司有合做关系, 须要嵌入对方的脚本时,这样能保证它本身编写的代码都在YAHOO这个空间下面,而其余公司不大可能在这个空间下面编码,就基本不会出现命名冲突的状况。

咱们通常的实现方式:

namespace = function(){
    var argus = arguments;
    for(var i = 0; i < argus.length; i++){
        var objs = argus[i].split(".");
        var obj = window;
        for(var j = 0; j < objs.length; j++){
            obj[objs[j]] = obj[objs[j]] || {};
            obj = obj[objs[j]];
        }
    }
    return obj;
};

namespace("tools.base");

固然咱们也经常利用js对象字面量的方式来实现js的命名空间:

var school = {
    addClass:function(classnum){
        console.log(classnum);
    },
    addStudent:function(stuId){
        console.log(stuId);
    }
}
school.addClass("2");

在全局做用域中声明的任何变量和函数都是window对象的属性,当名称有冲突时,就会产生一些不可控的问题。全局变量会带来如下问题:

  • 命名冲突
  • 代码的脆弱性
  • 难以测试

在编程开发中合理的使用命名空间,能够避免相同的变量或对象名称产生的冲突。并且,命名空间也有助于组织代码,有更强的可维护性和可读性。JavaScript中虽然没有提供原生的命名空间支持,但咱们可使用其余的方法(对象和闭包)实现相似的效果。下面就是一些常见的命名空间模式:

1.单一全局变量

JavaScript中一个流行的命名空间模式是选择一个全局变量做为主要的引用对象。由于每一个可能的全局变量都成为惟一全局变量的属性,也就不用再建立多个全局变量,那么也就避免了和其余声明的冲突。

单一全局变量模式已经在很多的JavaScript类库中使用,如:

  • YUI定义了惟一的YUI全局对象
  • jQuery定义了$jQuery$由其余类库使用时使用jQuery
  • Dojo定义了一个Dojo全局变量
  • Closure类库定义了一个goog全局对象
  • Underscore类库定义了一个_ 全局对象

示例以下:

var myApplication = (function() {
    var count = 1;
     function funcur() {
        console.log(count);
     };
     return {
        funcur:funcur
     }
 })();
myApplication.funcur();

虽然单一全局变量模式适合某些状况,但其最大的挑战是确保单一全局变量在页面中是惟一使用的,不会发生命名冲突

2.命名空间前缀

命名空间前缀模式其思路很是清晰,就是选择一个独特的命名空间,而后在其后面声明声明变量、方法和对象。示例以下:

var  myApplication_propertyA = {};
var  myApplication_propertyB = {};

function myApplication_myMethod() {
    // ***
}

从某种程度上来讲,它确实减小了命名冲突的发生几率,但其并无减小全局变量的数目。当应用程序规模扩大时,就会产生不少的全局变量。在全局命名空间内,这种模式对其余人都没有使用的这个前缀有很强的依赖,并且有些时候也很差判断是否有人已经使用某个特殊前缀,在使用这种模式时必定要特别注意。

3.对象字面量表示法

对象字面量模式能够认为是包含一组键值对的对象,每一对键和值由冒号分隔,键也能够是代码新的命名空间。示例以下:

var myApplication = {
    // 能够很容易的为对象字面量定义功能
    getInfo:function() {
        // ***
    },
    
    // 能够进一步支撑对象命名空间
    models:{},
    views:{
        pages:{}
    },
    collections:{}
};

与为对象添加属性同样,咱们也能够直接将属性添加到命名空间。对象字面量方法不会污染全局命名空间,并在逻辑上协助组织代码和参数。而且,这种方式可读性和可维护性很是强,固然咱们在使用时应当进行同名变量的存在性测试,以此来避免冲突。下面是一些经常使用的检测方法:

var myApplication = myApplication || {};

if(!myApplication) {
    myApplication  = {};
}

window.myApplication || (window.myApplication || {});

// 针对jQuery
var myApplication = $.fn.myApplication = function() {};

var myApplication = myApplication === undefined ? {} :myApplication;

对象字面量为咱们提供了优雅的键/值语法,咱们能够很是便捷的组织代码,封装不一样的逻辑或功能,并且可读性、可维护性、可扩展性极强。

4.嵌套命名空间

嵌套命名空间模式能够说是对象字面量模式的升级版,它也是一种有效的避免冲突模式,由于即便一个命名空间存在,它也不太可能拥有一样的嵌套子对象。示例以下:

var myApplication = myApplication || {};
 
 // 定义嵌套子对象
 myApplication.routers = myApplication.routers || {};
 myApplication.routers.test = myApplication.routers.test || {};

固然,咱们也能够选择声明新的嵌套命名空间或属性做为索引属性,如:

myApplication['routers'] = myApplication['routers'] || {};

使用嵌套命名空间模式,可使代码易读且有组织性,并且相对安全,不易产生冲突。其弱点是,若是咱们的命名空间嵌套过多,会增长浏览器的查询工做量,咱们能够把要屡次访问的子对象进行局部缓存,以此来减小查询时间。

5.当即调用的函数表达式

当即调用函数(IIFE)实际上就是匿名函数,被定义后当即被调用。在JavaScript中,因为变量和函数都是在这样一个只能在内部进行访问的上下文中被显式地定义,函数调用提供了一种实现私有变量和方法的便捷方式。IIFE是用于封装应用程序逻辑的经常使用方法,以保护它免受全局名称空间的影响,其在命名空间方面也能够发挥其特殊的做用。示例以下:

;(function (namespace, undefined) {

    // 私有属性
    var foo = "foo";
        bar = "bar";

    // 公有方法和属性
    namespace.foobar = "foobar";
    namespace.sayHello = function () {
        say("Hello World!");
    };

    // 私有方法
    function say(str) {
        console.log("You said:" + str);
    };
})(window.namespace = window.namespace || {});
console.log(namespace.foobar);
//foobar

可扩展性是任何可伸缩命名空间模式的关键,使用IIFE能够轻松实现这一目的,咱们能够再次使用IIFE给命名空间添加更多的功能。

6.命名空间注入

命名空间注入是IIFE的另外一个变体,从函数包装器内部为一个特定的命名空间“注入”方法和属性,使用this做为命名空间代理。这种模式的优势是能够将功能行为应用到多个对象或命名空间。示例以下:

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

;(function () {
    var value = 5;
    
    this.getValue = function () {
        return value;
    }

    // 定义新的子命名空间
    this.tools = {};
}).apply(myApplication.utils);

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

// 一样的方式在普通的IIFE上扩展功能,仅仅将上下文做为参数传递并修改,而不是仅仅使用this

还有一种使用API来实现上下文和参数天然分离的方法,该模式感受更像是一个模块的建立者,但做为模块,它还提供了一个封装解决方案。示例以下:

var ns = ns || {},
    ns1 = ns1 || {};

// 模块、命名空间建立者
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 此时已经存在

creator.call(ns1, 5000);
// ns1包含相同的方法,但值被重写为5000了

console.log(ns.next()); //0
console.log(ns1.next());//5000

命名空间注入是用于为多个模块或命名空间指定一个相似的功能基本集,但最好是在声明私有变量或者方法时再使用它,其余时候使用嵌套命名空间已经足以知足须要了。

7.自动嵌套的命名空间

嵌套命名空间模式能够为代码单元提供有组织的结构层级,但每次建立一个层级时,咱们也得确保其有相应的父层级。当层级数量很大时,会给咱们带来很大的麻烦,咱们不能快速便捷的建立想建立的层级。那么如何解决这个问题呢?Stoyan Stefanov提出,建立一个方法,其接收字符串参数做为一个嵌套,解析它,并自动用所需的对象填充基本名称空间。下面是这种模式的一种实现:

function extend(ns, nsStr) {
    var parts = nsStr.split("."),
        parent = ns,
        pl;

    pl = parts.length;

    for (var i = 0; i < pl; i++) {
        // 属性若是不存在,则建立它
        if (typeof parent[parts[i]] === "undefined") {
            parent[prats[i]] = {};
        }
        parent = parent[parts[i]];
    }
    return parent;
}

// 用法
var myApplication = myApplication || {};
var mod = extend(myApplication, "module.module2");

之前咱们必须为其命名空间将各类嵌套显式声明为对象,如今用上述更简洁、优雅的方式就实现了。

参考地址:JavaScript之命名空间模式 浅析

相关文章
相关标签/搜索