团队内部进行协同开发的时候,如果涉及到Javascript代码,可能会遇到函数名和类名冲突的情况。这时团队可以进行一些命名约定,比如在函数名前加相应的前缀区别,类似google igapi中使用的_IG_FetchXmlContent(),_IG_RegisterOnloadHandler()等。另外也可以引入命名空间的概念,但是Javascript原生并不支持命名空间,需要变通来实现。

在Javascript中,所有的对象(或者称类型,例如Boolean,Array,Function)都可以认为是一个关联数组。关联数组中的对象可以使用点(.)进行引用,这样我们可以利用关联数组变相地实现命名空间。首先声明一个关联数组作为根,因为页面声明的对象都是window这个变量的成员,所以一般命名空间的实现都以window为根。当向window申请a.b.c的命名空间时,首先在window中查看是否存在a这个成员,如果没有则在window下新建一个名为a的空关联数组,如果已经存在a,则继续在window.a中查看b是否存在,以此类推。下面分别是Atlas和YUI中的实现方法。

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

Atlas的实现通俗易懂。Javascrip中rootObject[currentPart]=new Object();和rootObject[currentPart]={};是等效的两种写法。

//YUI命名空间的实现方法
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的实现带有一点C风格,nsobj[levels[i]] = nsobj[levels[i]] || {};这句相比于Atlas显得要晦涩一些。

比较一下Atlas和YUI的实现,YUI稍微好一些,因为YUI中申请命名空间的时候会返回一个引用,可以赋值给一个变量,这就相当于声明了该名称空间的一个别名,编码会方便不少。YUI把所有申请的命名空间都放在了window.YAHOO下面,这样有什么好处呢?假如Yahoo和其他公司有合作关系,需要嵌入对方的脚本时,这样能保证它自己编写的代码都在YAHOO这个空间下面,而其他公司不大可能在这个空间下面编码,就基本不会出现命名冲突的情况。我觉得这个做法好,是因为在不对等的合作关系中,要求对方去修改代码来适应你的应用是不现实的,YUI的实现考虑了这点。

但是,我认为YUI的实现同样是有缺陷的。

和Atlas一样,YUI并没有返回命名空间重叠时的信息。例如在某个页面中的一个应用已经申请过YAHOO.Widget,第二个应用准备申请YAHOO.Widget.Tools,并打算在Tools下面新增一个Dictionary对象,但它并不知道其实在第一个应用申请YAHOO.Widget后已经在YAHOO.Widget下面增加了一个叫Tools的对象。第二个应用新增Dictionary时它还需要对YAHOO.Widget.Tools.Dictionary是否存在进行判断,如果不判断很有可能在YAHOO.Widget下的Tools对象里面就已经存在一个叫Dictionary的成员了,这样就会导致Dictionary被覆盖重写,而如果申请一个命名空间以后,在该空间下面每次新增对象都要对该对象是否存在进行判断,显然是不合理的。YUI并没有在这个方面进行考虑。

另外一点,所有的命名空间都放在window.YAHOO下是最好最合理的做法嘛?在大量应用动态效果的页面中,这势必要维护一个超大的关联数组。关联数组本质是哈希数组,检索数组成员的开销可以忽略不计,但是window.YAHOO承载的东西太多太复杂,这是否违背了大道至简的原则?有没有更好的办法?

在小规模的脚本开发中,有时候并不值得去引用命名空间,因为会带来某种程度的复杂性。除了命名约定和引入命名空间,我们还可以类似CSS一样进行区块化编码(当然这个称呼是我自己瞎编的,如有雷同,纯属巧合),将每一个小应用所包含的变量和成员封装在一个类或者变量中,这样能显著降低命名冲突的概率。示例如下

//声明变量,内部的成员使用this引用
var imageSwap = {
    handler:'is4357',
    render:function(){...},
    init:function(){...}
}
//声明类,适合页面有多个实例的情况
function imageSwap(handler){
    this.handler = handler!=null ? handler : 'is4357';
}
imageSwap.prototype = {
    render:function(){...},
    init:function(){...}
}

现在我们公司有些页面就使用了这种做法。