这又是一个新的开始,对象的重要性不言而喻。在JavaScript中创建对象是十分容易的,之前聊过的对象字面量和构造函数都可以达到目的。但是本篇中,我们越过那些方法,以寻求一些额外的对象创建模式。
本篇内容,我们将学到命名空间(namespace),依赖声明(dependency declaration)、模块模式(module pattern)、沙箱模式(sandbox pattern)。它们都可以帮助您组织应用程序代码的结构,并且降低隐含的全局变量带来的后果。其他讨论的主题包括私有和特权成员、对象常量、链和一个启发类的方式以以定义构造函数。
一、命名空间模式
命名空间(namespace)有助于减少程序中所需要的全局变量的数量,并且同时还有助于避免命名冲突或过长的名字前缀。
JavaScript语言的语法中并没有内置命名空间,但是这种特征是非常容易实现的。可以为应用程序或库创建一个(理想上最好只有一个)全局对象,然后可以将所有功能添加到该全局对象中,从而在有大量函数、对象和其他变量的情况下并不会污染全局范围。
// 5个全局变量 // 警告:反模式 // 构造函数 function Parent() {} function Child() {} // 一个变量 var some_var = 1; // 一些对象 var module1 = {}; module1.data = {a:1,b:2}; var module2 = {}; // 可以通过为应用程序创建一个全局对象这种方式来重构上面这种类型的代码: // 之后:1个全局变量 // 全局变量 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 = {};
对于全局命名空间对象的名称,可以任意选择,比如以应用程序或库的名称、域名或公司名来命名。通常根据公约,以全部大写的方式来命名全局变量,这样比较显眼(当然,常量也是这样)。
这种模式是一种组织代码的命名空间的好方法,不仅可以避免您代码中的命名冲突,并且还可以避免在同一个页面中您的代码和第三方代码之间的命名冲突。
当然,这样的方式也有一些缺点:
- 需要输入更多的字符,每个变量和函数前都要附加前缀,总体上增加了需要下载的代码量。
- 仅有一个全局实例意味着任何部分的代码都可以修改该全局实例。
- 长嵌套名字意味着更长(更慢)的属性解析查询时间。
本篇后面介绍的沙箱模式可以解决以上这些缺点。
通用命名空间函数
由于程序负责性的增加、代码的某些部分被分割成不同的文件,以及使用条件包含语句等多个因素,仅假设您的代码是第一个定义某个命名空间或它内部的属性,这种做法已经变得不再安全。添加到命名空间的一些属性可能已经存在,这导致可能会覆盖它们。因此,在添加一个属性或者创建一个命名空间之前,最好是首先检查它是否已经存在:
// 不安全的代码 var MYAPP = {}; // 更好的代码风格 if(typeof MYAPP === 'undefined') { var MYAPP = {}; } // 或者用更短的语句 var MYAPP = MYAPP || {};
可以看到这些附加的检查是如何循序导致大量的重复代码。比如,如果想要定义MYAPP.modules.module2,必须构造三次检查,每次检查都要针对定义的一个对象或者属性。这也就是为什么需要一个可以很方便地处理命名空间细节的可重用函数的原因。让我们称该函数为namespace()并以如下方式使用:
// 使用命名空间函数 MYAPP.namespace('MYAPP.modules.module2'); // 相当于以下代码 var MYAPP = { modules:{ module2:{} } };
接下来是一个命名空间函数的示例。这个实现是非破坏性的,也就是说,如果已经存在一个命名空间,便不会再重新创建它:
var MYAPP = MYAPP || {}; MYAPP.namespace = function (ns_string) { var parts = ns_string.split('.'), parent = MYAPP, i; // 剥离最前面的冗余全局变量 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'); console.log(module2 === MYAPP.modules.module2); // 忽略最前面的‘MYAPP’ console.log(MYAPP.namespace('modules.module51')); // 长命名空间 console.log(MYAPP.namespace('once.upon.a.time.there.was.this.long.short.nested.property'));
二、声明依赖关系
JavaScript库通常是模块化且依据命名空间组织的,这使您能够仅包含所需的模块。例如,在YUI2库中有一个充当命名空间的全局变量YAHOO,而模块是该全局变量的属性,比如YAHOO.util.Dom和YAHOO.util.Event。
在您的函数或模块顶部声明代码所依赖的模块是一个非常好的主意。该声明仅涉及创建一个局部变量并使其指向所需的模块。
var myFunction = function () { // 依赖 var event = YAHOO.util.Event, dom = YAHOO.util.Dom; // 使用事件和DOM变量 // 其他逻辑 };
这是一个及其简单的模式,但是它却有很多优点:
- 显式的依赖声明向您代码的用户表明了他们确定需要的特定脚本文件已经包含在该页面中。
- 在函数顶部的前期声明可以使您很容易地发现并解析依赖。
- 解析局部变量的速度总是要比解析全局变量要快,甚至比使用全局变量的嵌套属性还要快,这导致了更好的性能。当使用这种依赖声明模式时,全局符号解析仅会在函数中执行一次。在此之后将会使用局部变量,这种解析速度也快很多。
- 类似于YUICompressor和Google闭包编译器的这些高级小工具可以重命名局部变量(即,压缩),这导致了更小的代码量,但是这些工具从不会对全局变量进行重命名,因为这样做是不安全的。
这篇就到这里了,说实话就是很小的两个点,但是这两个point,确实会给你的代码带来不错的性能提升。好了,下篇文章见。