单体模式作为一种软件开发模式在众多面向对象语言中得到了广泛的使用,在javascript中,单体模式也是使用非常广泛的,但是由于javascript语言拥有其独特的面向对象方式,导致其和一些传统面向对象语言虽然在单体模式的思想上是一致的,但是实现起来还是有差异的。
首先来看看传统面向对象语言对于单体模式的定义:单体模式是只能被实例化一次并且可以通过一个众所周知的访问点来访问的类。这个定义有两点突出了传统面向对象语言的特征,即类和实例化,所以对于传统面向对象语言来讲,单体模式是建立在其类和实例化的自然特性之上的,即使用关键字class定义一个类,该类可通过new关键字来实例化,但是需要保证每次被new实例化之后得到的都是同一个实例或者说只能通过new来调用其构造函数一次。
再来看看javascript中对于单体模式的定义:单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它能够被实例化,那么只能被实例化一次。对比上面的定义,你会发现这里的单体定义将其实质定义为对象,而不是传统面向对象语言中的类,这也表明了javascript这门语言是基于对象的。同时后面又指出,如果能够被实例化,这说明了在javascript中单体定义应该有好几种方式,存在一种或几种能够被实例化即使用new关键字来创建单体对象的方式,但是这种方式不是javascript自身的自然特征,因为使用new关键字创造出来的对象,实际上都是通过function来模拟定义其构造函数的(虽然ES6开始支持class关键字了,但是目前还没有得到浏览器广泛支持),那么如何使用javascript的自然特征来实现单体模式呢?单体模式的基本结构如下:
1 var Singleton = { 2 attr1: 1, 3 attr2: 2, 4 method1: function() { 5 alert(this.attr1); //将方法绑定到事件监听器上,this指向的dom元素就会失效,这里最好使用单体对象的全名访问属性和方法 6 }, 7 method2: function(arg) { 8 } 9 };
绑定事件测试代码如下:
1 <html> 2 <body> 3 <input id="btntest" type="button" value="测试" /> 4 </body> 5 </html> 6 <script> 7 var elem = document.getElementById("btntest"); 8 elem.addEventListener("click", Singleton.method1); //undefined 9 </script>
基本结构中定义了一个对象Singleton,内部包含若干属性和方法,将其包含在页面中,js载入的时候就创建了这个对象,在调用时使用Singleton.method1来调用,它的实例化是随着页面载入js解析执行过程中完成的,我们并没有使用new关键字来实例化这个对象,这也是javascript中实现单体模式和传统面向对象语言一个很大的不同。这种方式更为简单易于理解。但是这种方式存在若干缺点,一个很明显的缺点是它并没有提供命名空间,其他程序员如果在页面中也定义了一个Singleton变量,那么很容易改写和混淆这个单体对象,于是针对这个问题,将其改写如下:
1 var namespace={}; 2 namespace.Singleton={ 3 attr1:true, 4 attr2:10, 5 method1:function(){ 6 }, 7 method2:function(arg){ 8 } 9 }
这里首先定义了一个namespace的命名空间,然后将单体对象Singleton挂载在这个对象的下面,这大大减少了和其他程序员冲突以及误操作的可能,即使其他人在全局作用域中定义一个Singleton变量,也不会污染到这个单体对象,这就实现了前面定义中所说的划分命名空间并且将一些相关属性和方法组织在一起的功能。
这个方法依然存在缺点,这个单体对象的所有属性和方法都是共有的,外部可随时访问和修改,于是采用闭包来模拟私有属性和方法,如下:
1 namespace.Singleton = (function() { 2 var privateAttr1 = false; 3 var privateAttr2 = [1, 2, 3]; 4 function privateMethod1() { 5 6 } 7 function privateMethod2() { 8 9 } 10 return { 11 publicAttr1: true, 12 publicAttr2: 10, 13 publicMethod1: function() { 14 privateAttr1 = true; 15 privateMethod1(); 16 }, 17 publicMethod2: function(arg) { 18 privateAttr2 = [4, 5, 6]; 19 privateMethod2(); 20 } 21 } 22 })();
在这里我们直接给该单体对象赋值了一个匿名自执行的函数,在该函数中使用var和function关键字分别来定义其私有属性和方法,这些在函数外部(单体对象外部)是无法直接访问的,因为函数一执行完毕,其内部作用域的空间就会被回收,这也就是能够利用闭包来模拟私有属性和方法的原因所在。在该函数(闭包)中,同时最终返回一个对象,这个对象中包含一些公有方法和属性,在外部可以直接调用,同时这些公有方法由于定义在函数内部,所以可以调用其私有属性和方法,但是外界只能通过返回的公有方法和属性来完成某些操作,不能够直接调用Singleton.privateMethod1这些属性。这就使得该单体对象既隔离了外界去直接访问其私有属性和方法,又提供给外界一些共有属性和方法去完成某些操作。
这种匿名函数自执行所构造的单体模式在很多js库中被广泛使用,但是依然存在一个问题,如果我们在载入页面的时候并不需要用到该对象,而且该对象的创建比较耗费开销(如需要进行大量计算或需要多次访问dom树及其属性等)时,合理的做法是需要它的时候再去创建它,而不是随着js的解析执行直接去创建,这种概念被称之为惰性加载(lazy loading),于是修改以上代码如下:
1 var Singleton = (function() { 2 var instantiated; //匿名函数创建私有变量,判断单体对象是否被创建的句柄 3 function init() { 4 return { 5 publicMethod: function() { 6 alert('hello word'); 7 }, 8 publicPrototype: "test" 9 }; 10 } 11 return { 12 getinstance: function() { 13 if (!instantiated) { 14 instantiated = new init(); 15 } 16 return instantiated; 17 } 18 } 19 })(); 20 Singleton.getinstance().publicMethod();
这里首先在匿名函数中定义了一个私有变量instantiated,作为一个判断单体对象是否被创建出来的句柄,然后将刚才所有对单体对象定义的属性和方法都放在一个名为init的函数中,只有该函数调用了,才会创造出该单体对象,否则不会直接创建它。然后,返回一个对象,其包含一个getInstance方法,该方法是供外部调用的,调用该方法的时候首先判断该单体对象是否存在,如果存在就直接返回它,否则调用init函数构造这个单体对象再返回它。最后如果我们调用该单体对象的某个方法,需要使用Singleton.getInstance().publicMethod(),这里,只有我们这样调用的时候才会创建这个单体对象,否则该单体对象是不会被自动创建的,这实际上就实现了按需加载或者惰性加载。