命名空间模式

使用命名空间可以减少全局变量的数量,与此同时,还能有效地避免命名冲突和前缀的滥用。

JavaScript没有原生的命名空间语法,但很容易可以实现这个特性。为了避免产生全局污染,你可以为应用或者类库创建一个(通常是唯一一个)全局对象,然后将所有的功能都添加到这个对象上,而不是到处声明大量的全局函数、全局对象以及其他的全局变量。

看如下例子:

  1. // 重构前:5个全局变量
  2. // 注意:反模式
  3. // 构造函数
  4. function Parent() {}
  5. function Child() {}
  6. // 一个变量
  7. var some_var = 1;
  8. // 一些对象
  9. var module1 = {};
  10. module1.data = {a: 1, b: 2};
  11. var module2 = {};

可以通过创建一个全局对象(通常代表应用名)比如MYAPP来重构上述这类代码,然后将上述例子中的函数和变量都变为该全局对象的属性:

  1. // 重构后:一个全局变量
  2. // 全局对象
  3. var MYAPP = {};
  4. // 构造函数
  5. MYAPP.Parent = function () {};
  6. MYAPP.Child = function () {};
  7. // 一个变量
  8. MYAPP.some_var = 1;
  9. // 一个对象容器
  10. MYAPP.modules = {};
  11. // 嵌套的对象
  12. MYAPP.modules.module1 = {};
  13. MYAPP.modules.module1.data = {a: 1, b: 2};
  14. MYAPP.modules.module2 = {};

这里的MYAPP就是命名空间对象,对象名可以随便取,可以是应用名、类库名、域名或者是公司名都可以。开发者经常约定全局变量都采用大写(所有字母都大写),这样可以显得比较突出(不过要记住,大写的变量也常用于表示常量)。

这种模式是一种很好的提供命名空间的方式,避免了自身代码的命名冲突,同时还避免了同一个页面上自身代码和第三方代码(比如JavaScript类库或者widget)的冲突。这种模式在大多数情况下非常适用,但也有它的缺点:

  • 代码量稍有增加;在每个函数和变量前加上这个命名空间对象的前缀,会增加代码量,增大文件大小
  • 该全局实例可以被随时修改
  • 命名的深度嵌套会减慢属性值的查询

本章后续要介绍的沙箱模式则可以避免这些缺点。

通用命名空间函数

随着程序复杂度的提高,代码会被分拆在不同的文件中以按照页面需要来加载,这样一来,就不能保证你的代码一定是第一个定义命名空间或者某个属性的,甚至会发生属性覆盖的问题。所以,在创建命名空间或者添加属性的时候,最好先检查下是否存在,如下所示:

  1. // 不安全的做法
  2. var MYAPP = {};
  3. // 更好的做法
  4. if (typeof MYAPP === "undefined") {
  5. var MYAPP = {};
  6. }
  7. // 简写
  8. var MYAPP = MYAPP || {};

如上所示,如果每次做类似操作都要这样检查一下就会有很多重复的代码。例如,要声明MYAPP.modules.module2,就要重复三次这样的检查。所以,我们需要一个可复用的namespace()函数来专门处理这些检查工作,然后用它来创建命名空间,如下所示:

  1. // 使用命名空间函数
  2. MYAPP.namespace('MYAPP.modules.module2');
  3. // 等价于:
  4. // var MYAPP = {
  5. // modules: {
  6. // module2: {}
  7. // }
  8. // };

下面是上述namespace函数的实现示例。这种实现是非破坏性的,意味着如果要创建的命名空间已经存在,则不会再重复创建:

  1. var MYAPP = MYAPP || {};
  2. MYAPP.namespace = function (ns_string) {
  3. var parts = ns_string.split('.'),
  4. parent = MYAPP,
  5. i;
  6. // 去除不必要的全局变量层
  7. // 译注:因为namespace已经属于MYAPP
  8. if (parts[0] === "MYAPP") {
  9. parts = parts.slice(1);
  10. }
  11. for (i = 0; i < parts.length; i += 1) {
  12. // 如果属性不存在则创建它
  13. if (typeof parent[parts[i]] === "undefined") {
  14. parent[parts[i]] = {};
  15. }
  16. parent = parent[parts[i]];
  17. }
  18. return parent;
  19. };

上述实现支持如下几种用法:

  1. // 将返回值赋给本地变量
  2. var module2 = MYAPP.namespace('MYAPP.modules.module2');
  3. module2 === MYAPP.modules.module2; // true
  4. // 省略全局命名空间`MYAPP`
  5. MYAPP.namespace('modules.module51');
  6. // 长命名空间
  7. MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');

图5-1 展示了上述代码创建的命名空间对象在Firebug下的可视化结果

MYAPP命名空间在Firebug下的可视结果

图5-1 MYAPP命名空间在Firebug下的可视化结果