命名空间管理


ECMAScript 6 才有规范模块语法,在这之前,JavaScript 本身没有命名空间管理的机制,名称都是对象上的特性,要不就是全局对象上的特性(全局变量),要不就是 context 对象上的变量(局部变量)。

名称冲突的问题极容易在 JavaScript 中发生,就算是在同一个 .js 文件中也有可能发生。例如你也许写了个validate函数,假以时日别人接手你的程序,然后在文件中某处又定义了另一个validate函数:

function validate() {
    //.. 作些验证
}
// 很长很长的程序。。
// 某年某月的某一天。。
function validate() {
    //.. 作些别的验证
}

很不幸地,之后的validate函数定义会覆盖前一个函数,也许会让之前可以动作的功能失效。

最基本的命名空间管理,就是将这个函数作为某对象的特性,该对象是对组织或单位有意义的名称所参考。例如:

var openhome = {};     // 作为命名空间
function validate() {
    //.. 作些验证
}
openhome.validate = validate;

想要取用你定义的validate函数,则可以如下:

openhome.validate();

其他人在定义函数时,也可以作类似考量。例如他也许在同一个 .js 中如下定义:

var caterpillar = {};     // 作为命名空间
function validate() {
    //.. 作些验证
}
caterpillar.validate = validate;

调用时就使用:

caterpillar.validate();

如此就不会发生名称覆盖的问题。或许在同一个 .js 中,以上名称冲突的情况比较算少数,通常这会发生在两个 .js 分别由两个不同组识或作者编写时。

以上是在 ES6 之前,进行命名空间处理的出发点,也就是透过特定对象来收纳相关 API,然后该对象被指定给一个名称。

在上面的范例中,validate的函数定义,实际上还是在全局占用了一个 validate 名称,你可以这么解决:

var openhome = {};
openhome.validate = function() {
    // 作些验证
};

但如果这个函数也想作为其他对象上的特性,函数字面量的作法会比较不方便。有个方式可以比较漂亮地解决。例如:

var openhome = (function() {
    function validate() {
        // 作些验证
    }

    function fomrat() {
        // 作些格式化
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
})();

乍看有些复杂,事实上,首先是编写..

function() {
}

这写下了一个函数字面量,没有名称参考至它,所以不会污染全局命名空间…接着…

(function() {
})

加上括号是语法需求,这样 JavaScript 引擎才知道这边是个函数字面量,接着…

(function() {
})();

最后的括号表示调用返回的函数对象,有人称这样的写法为 IIFE(Immediately Invoked Function Expression),也就是所谓的立即调用函数,在这个函数中创建的名称,范围都是在函数中,不会污染全局命名空间,到最后返回的对象被指定给openhome变量。

通常你会在以上的模式中,编写一个模块,也就是将功能相关的代码写在 IIFE 中,如果想要扩充这个模块,同样也可以在 IIFE 中进行。例如:

var openhome = (function() {
    function validate() {
        // 作些验证
    }

    function fomrat() {
        // 作些格式化
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
})();

// 扩充模块
(function(module) {
    function find() {
        // ...找些东西
    }

    function map() {
        // ... 作些对应
    }

    // .... 其他

    module.find = find;
    module.map = map;

})(openhome);

这么一来,就可以基于原本的模块来扩充功能,最后,可以来做些正事了:

(function(module) {

    // 应用程序流程

})(openhome);

现在的问题在于,如果连 openhome 这样的名称都不想占用呢?那么需要有个模块管理程序,来负责管理这些名称,例如,一个最简单的实现是:

var define, require;

(function() {
    var modules = {};

    define = function(name, callback) {
        modules[name] = callback();
    };

    require = function(name, callback) {
        callback(modules[name]);
    };
})();

接下来,如果要定义一个模块,例如,仍然是定义openhome模块,就可以使用define

define('openhome', function() {
    function validate() {
        console.log('validate');
    }

    function format() {
        console.log('format');
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
});

而另一个模块,想要扩充openhome模块,或者依赖于openhome模块来做些事情,可以使用require

require('openhome', function(openhome) {
    openhome.validate();
    openhome.format();
    // ... 其他
});

当然,依赖的模块也许不只一个,这就需要修改一下require

var define, require;

(function() {
    var modules = {};

    define = function(name, callback) {
        modules[name] = callback();
    };

    require = function(names, callback) {
        var dependencies = names.map(function(name) {
            return modules[name];
        });

        callback.apply(undefined, dependencies);
    };
})();

define('openhome', function() {
    function validate() {
        console.log('validate');
    }

    function format() {
        console.log('format');
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
});

define('caterpillar', function() {
    function foo() {
        console.log('foo');
    }

    // ... 其他...

    return {
        foo : foo
    };
});

require(['openhome', 'caterpillar'], function(openhome, caterpillar) {
    openhome.validate();
    openhome.format();
    caterpillar.foo();
});

如果在浏览器的环境中,模块管理程序可能是放在一个 require.js 里:

var define, require;

(function() {
    var modules = {};

    define = function(name, callback) {
        modules[name] = callback();
    };

    require = function(names, callback) {
        var dependencies = names.map(function(name) {
            return modules[name];
        });

        callback.apply(undefined, dependencies);
    };
})();

而你会在 openhome.js 放入:

define('openhome', function() {
    function validate() {
        console.log('validate');
    }

    function format() {
        console.log('format');
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
});

在 caterpillar.js 中放入:

define('caterpillar', function() {
    function foo() {
        console.log('foo');
    }

    // ... 其他...

    return {
        foo : foo
    };
});

接着写一个 main.js:

require(['openhome', 'caterpillar'], function(openhome, caterpillar) {
    openhome.validate();
    openhome.format();
    caterpillar.foo();
});

最后,在网页中,你会这么编写:

<script type="text/javascript" src="require.js"></script>
<script type="text/javascript" src="openhome.js"></script>
<script type="text/javascript" src="caterpillar.js"></script>
<script type="text/javascript" src="main.js"></script>

当然,安排模块 .js 文件的顺序也会是个问题,你可以继续依需求来重构下去,直到满足需求为止,实际上,已经有许多成熟的模块实现,可以用来管理命名空间,而且进一步地处理了模块依赖、加载等需求,例如,在浏览器的环境中,许多开发者熟悉的是 RequireJS,它实现了 AMD(Asynchronous Module Definition)。

而在 Node.js 中,实现的是 CommonJS 的模块规范,另外,也存在着 UMD(Universal Module Definition)的形式,可以在 AMD 和 CommonJS 之间沟通。

ES6 本身也提供了模块方面的规范,然而在后端,与 Node.js 现存的 CommonJS 有许多不同,而在前端,一些长青(Even-green)浏览器,实现了 ES6 模块的规范,不过考虑到浏览器之间的兼容性,使用 RequireJS 会是比较保险的做法。

或者,你可以固定采用某个规范,然而在必要时,借助转译或模块封装工具(像是 Babel 或 Webpack),将模块转换为另一个环境或规范。


展开阅读全文