命名空间模式
使用命名空间可以减少全局变量的数量,与此同时,还能有效地避免命名冲突和前缀的滥用。
JavaScript没有原生的命名空间语法,但很容易可以实现这个特性。为了避免产生全局污染,你可以为应用或者类库创建一个(通常是唯一一个)全局对象,然后将所有的功能都添加到这个对象上,而不是到处声明大量的全局函数、全局对象以及其他的全局变量。
看如下例子:
// 重构前:5个全局变量
// 注意:反模式
// 构造函数
function Parent() {}
function Child() {}
// 一个变量
var some_var = 1;
// 一些对象
var module1 = {};
module1.data = {a: 1, b: 2};
var module2 = {};
可以通过创建一个全局对象(通常代表应用名)比如MYAPP
来重构上述这类代码,然后将上述例子中的函数和变量都变为该全局对象的属性:
// 重构后:一个全局变量
// 全局对象
var MYAPP = {};
// 构造函数
MYAPP.Parent = function () {};
MYAPP.Child = function () {};
// 一个变量
MYAPP.some_var = 1;
// 一个对象容器
MYAPP.modules = {};
// 嵌套的对象
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};
这里的MYAPP
就是命名空间对象,对象名可以随便取,可以是应用名、类库名、域名或者是公司名都可以。开发者经常约定全局变量都采用大写(所有字母都大写),这样可以显得比较突出(不过要记住,大写的变量也常用于表示常量)。
这种模式是一种很好的提供命名空间的方式,避免了自身代码的命名冲突,同时还避免了同一个页面上自身代码和第三方代码(比如JavaScript类库或者widget)的冲突。这种模式在大多数情况下非常适用,但也有它的缺点:
- 代码量稍有增加;在每个函数和变量前加上这个命名空间对象的前缀,会增加代码量,增大文件大小
- 该全局实例可以被随时修改
- 命名的深度嵌套会减慢属性值的查询
本章后续要介绍的沙箱模式则可以避免这些缺点。
通用命名空间函数
随着程序复杂度的提高,代码会被分拆在不同的文件中以按照页面需要来加载,这样一来,就不能保证你的代码一定是第一个定义命名空间或者某个属性的,甚至会发生属性覆盖的问题。所以,在创建命名空间或者添加属性的时候,最好先检查下是否存在,如下所示:
// 不安全的做法
var MYAPP = {};
// 更好的做法
if (typeof MYAPP === "undefined") {
var MYAPP = {};
}
// 简写
var MYAPP = MYAPP || {};
如上所示,如果每次做类似操作都要这样检查一下就会有很多重复的代码。例如,要声明MYAPP.modules.module2
,就要重复三次这样的检查。所以,我们需要一个可复用的namespace()
函数来专门处理这些检查工作,然后用它来创建命名空间,如下所示:
// 使用命名空间函数
MYAPP.namespace('MYAPP.modules.module2');
// 等价于:
// var MYAPP = {
// modules: {
// module2: {}
// }
// };
下面是上述namespace
函数的实现示例。这种实现是非破坏性的,意味着如果要创建的命名空间已经存在,则不会再重复创建:
var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
var parts = ns_string.split('.'),
parent = MYAPP,
i;
// 去除不必要的全局变量层
// 译注:因为namespace已经属于MYAPP
if (parts[0] === "MYAPP") {
parts = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
// 如果属性不存在则创建它
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};
上述实现支持如下几种用法:
// 将返回值赋给本地变量
var module2 = MYAPP.namespace('MYAPP.modules.module2');
module2 === MYAPP.modules.module2; // true
// 省略全局命名空间`MYAPP`
MYAPP.namespace('modules.module51');
// 长命名空间
MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');
图5-1 展示了上述代码创建的命名空间对象在Firebug下的可视化结果
图5-1 MYAPP命名空间在Firebug下的可视化结果