前面一章讲解的是单例的两种构建方法。下面就说说单例在JS怎么个最流行、最实用。
划分命名空间
单体对象由两个部分组成:包含方法和属性成员的对象自身,以及用于访问它的变量。
它提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问(通过确保单体对象只存在一份实例,你就可以确信自己的所有代码使用的都是同样的全局资源)。
命名空间是可靠地JavaScript编程的一个重要工具。把相关的方法组织到一起,也助于增强代码的文档性。
JavaScript 的命名空间并不是真正的命名空间, 只是在脚本内部创建一个封闭的小空间, 必须通过特定的空间名称才能对空间内部的代码进行访问, 这样可以防止同名函数和变量发生冲突, 也可以更方便地管理代码, 就像 .NET 的命名空间 (namespace) 和 Java 的包 (package) 一样.
用作特定页面专用代码的包装器
在拥有许多网页的网站中,有些JavaScript代码是所有网页都要用到的,它们通常被存放在独立的文件中;而有些代码则是某个页面专用的,不会被用到其他地方。最好把这两种代码分别包装在自己的单体对象中。
具体做法:封装一些数据(也许是作为常量)、为各页面的行为定义一些方法以及初始化方法。涉及DOM中特有元素的大多数代码,比如添加事件监听的代码,只有在这些元素加载之后才能工作。你可以通过创建一个init方法并将其关联到窗口的load事件(或类似的其他事件,如DOMContentLoaded事件),将所有这些初始化代码组织到一个地方。
/* Generic page Object */ var Namespace = {}; Namespace.PageName = { //页面常量constants CONSTANT_1: true, CONSTANT_2: 10, //Page methods method1: function() { //... }, method2: function(arg) { //... }, //可以把与DOM打交道的程序放在这里 init: function() { //... } }; addLoadEvent(Namespace.PageName.init);
创建私有成员
拥有真正的私有方法的一个缺点在于它们比较耗费内存,因为每个实例都具有方法的一份新副本。不过,由于单体对象只会被实例一次,因此为其定义真正的私有方法时不必考虑内存方面的问题。
/* 创建拥有私有成员单体的结构 */ var Namespace = {}; //自定义命名空间 Namespace.Singleton = {}; /* 用定义后立即执行函数创建单体 */ Namespace.Singleton = (function() { //... return {}; //返回字面量对象 })();
下面是一个具体的实例,描述了拥有私有方法单体的使用。
//使用闭包创建私有成员 Namespace.DataParse = (function() { //private method function stripWhitespace(str) { return str.replace(/\s+/g, ''); } function stringSplit(str, delimiter) { return str.split(delimiter); } //特权方法 return { stringToArray: function(str, delimiter, stripWS) { if(stripWS) { str = stripWhitespace(str); } return stringSplit(str, delimiter); } } })();
这种单体模式又称模块模式:它可以把一批相关的方法和属性组织为模块并起到划分命名空间的作用。
使用这种模式时,你可以享受到真正的私有成员带来的所有好处,而不必付出什么代价。这是因为单体类只会实例化一次。
单体中使用分支技术
一种用来把浏览器间的差异封装到在运行期间进行设置的动态方法中的技术。
var Namespace = {}; Namespace.Singleton = (function() { var objectA = { method1: function() {}, method2: function() {} }; var objectB = { method1: function() {}, method2: function() {} }; return (someCondtion)? objectA:objectB; })();
分支技术并不总是更高效的选择。在前面的例子中,有两个对象(objectA和objectB)被创建出来并保存在内存中,但派上用场的只有一个。在考虑是否使用这个技术的时候,需要在缩短计算时间和占用更多内存这一利一弊之间权衡一下。
示例:用分支技术创建XHR对象
var SimpleXHRFactory = (function() { //下面有三种情况,三个分支 var standard = { createXhrObject: function() { return new XMLHttpRequest(); } }; var activeXNew = { createXhrObject: function() { return new ActiveXObject('Msxml2.XMLHTTP'); } }; var activeXOld = { createXhrObject: function() { return new ActiveXObject('Microsoft.XMLHTTP'); } }; //利用try...catche判断适合的XHR对象 var XHR = null; try{ XHR = standard.createXhrObject(); return standard; //如果没有错误抛出,将返回该对象 }catch(e){ try { XHR = activeXNew.createXhrObject(); return activeXNew; }catch(e) { try { XHR = activeXOld.createXhrObject(); return activeXOld; }catch(e) { throw new Error('无法创建XHR对象'); } } } })(); //--------------------- test ------- console.log(SimpleXHRFactory.createXhrObject()); console.log(SimpleXHRFactory.createXhrObject());
单体模式提供的只是一种单点访问,有可能会导致模块间的强耦合。有时创建一个可实例化的类更可取,哪怕它只会被实例化一次。
有时候某种更高的模式会更符合任务的需要。与惰性加载单例相比,虚拟代理能给予对类实例化方式更多的控制权。也可以用工厂模式来取代分支型单体。