《javascript设计模式与开发实践》阅读笔记(4)—— 单例模式

定义

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

具体来说,就是保证有些对象有且只有一个,比如线程池、全局缓存、浏览器中的window 对象等。在js中单例模式用途很广,比如登录悬浮窗,我希望无论我点击多少次这个浮窗都只会被创建一次,这里就可以用单例模式。

1.实现单例模式

思路:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象;如果否就创建出那个对象。

 1 var Singleton = function( name ){  //构造函数
 2     this.name = name;         
 3     this.instance = null;     
 4 };
 5 Singleton.prototype.getName = function(){  //构造器原形上添加方法,可以获得对象的name属性
 6     alert ( this.name );
 7 };
 8 Singleton.getInstance = function( name ){
 9     if ( !this.instance ){       //如果不存在对象实例
10         this.instance = new Singleton( name );   //创建对象实例
11     }
12     return this.instance;   //返回对象实例
13 };
14 var a = Singleton.getInstance( 'sven1' );
15 var b = Singleton.getInstance( 'sven2' ); 
16 alert ( a === b );            // true
17 console.log(a.name);       // sven1
18 console.log(b.name);       // seven1
19 console.log(a.instance);   // null
20 console.log(b.instance);   // null
21 console.log(window.instance);  // Singleton {name: "sven1", instance: null}

这是书里的例子,从最下面的测试来看,这个例子其实并不太好,比较容易让人误会。我们改造一下,其实是一个意思:

 1     var Singleton = function( name ){  //构造函数
 2         this.name = name;         
 3         this.instance = null;     //无效的一个属性
 4     };
 5     Singleton.prototype.getName = function(){  //构造器原型上添加方法,可以获得对象的name属性
 6         alert ( this.name );
 7     };
 8     create = function( name ){       //全局创建对象的函数
 9         if ( !this.sing ){       //这里的this指向的是window,即全局,如果全局不存在sing对象
10             this.sing = new Singleton( name );   //创建sing对象
11         }
12         return this.sing;   //返回sing对象
13     };
14     var a = create( 'sven1' );
15     var b = create( 'sven2' ); 
16     alert ( a === b );            // true
17     console.log(a.name);       // sven1
18     console.log(b.name);       // seven1
19     console.log(a.instance);   // null
20     console.log(b.instance);   // null
21     console.log(window.sing);  // Singleton {name: "sven1", instance: null}

书里还有第二个创建单例模式的例子,如下:

 1     var Singleton = function( name ){  //构造函数
 2         this.name = name;
 3     };
 4     Singleton.prototype.getName = function(){   //原型上添加一个方法,可以返回对象的name属性
 5         alert ( this.name );
 6     };
 7     Singleton.getInstance = (function(){    //全局的一个自执行函数,自执行是为了把返回的函数字面量赋给Singleton.getInstance
 8         var instance = null;                //函数内部变量,但用闭包保存起来
 9         return function( name ){
10             if ( !instance ){   //如果没有创建过对应对象,即函数的这个内部变量没有被赋值
11                 instance = new Singleton( name );    //创建对象
12             }
13             return instance;     //返回对象
14         }
15     })();

这个例子比上面的要好不少,作者表示之所以用Singleton.getInstance这样的命名,是故意的,故意用这样的方式来创建单例类,来和通过new XXX的方式获取到的对象区分开,创建单例类必须要保存单例,所以需要定义一个全局变量,这种方式其实很不友好。

2.改进单例模式

现在的目标是实现一个“透明”的单例类,利用闭包保存单例,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样,这种方式较上面友好许多。

 1     var CreateDiv = (function(){   //匿名自执行函数,同时返回一个函数,创造了一个闭包环境
 2         var instance;        //利用闭包存储的对象实例
 3         var CreateDiv = function( html ){    //返回的函数
 4             if ( instance ){       //如果对象存在,返回对象
 5                 return instance;
 6             }
 7             this.html = html;       //不存在就赋值,创建
 8             this.init();
 9             return instance = this;   //new的时候返回实例
10         };
11         CreateDiv.prototype.init = function(){    //原型上绑定的方法
12             var div = document.createElement( 'div' );
13             div.innerHTML = this.html;
14             document.body.appendChild( div );
15         };
16         return CreateDiv;
17     })();
18 
19     var a = new CreateDiv( 'sven1' );
20     var b = new CreateDiv( 'sven2' );
21     alert ( a === b ); // true

缺点:为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton 构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。

1     var CreateDiv = function( html ){
2         if ( instance ){
3             return instance;
4         }
5         this.html = html;
6         this.init();
7         return instance = this;
8     };

观察这段构造函数,它实际上做了两件事,第一是创建对象和执行初始化init方法,第二是保证只有一个对象。这其实是种不好的做法,应该尽量遵循“单一职责原则”,假设我们某天需要利用这个类,在页面中创建千千万万的div,即要让这个类从单例类变成一个普通的可产生多个实例的类,那我们必须得改写CreateDiv 构造函数,把控制创建唯一对象的那一段去掉,这种修改会给我们带来不必要的烦恼。

3.用代理实现单例模式

通过引入代理类的方式,来解决上面提到的问题。
在CreateDiv 构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类,如下:

1     var CreateDiv = function( html ){
2         this.html = html;
3         this.init();
4     };
5     CreateDiv.prototype.init = function(){
6         var div = document.createElement( 'div' );
7         div.innerHTML = this.html;
8         document.body.appendChild( div );
9     };

接下来引入代理类SingletonCreateDiv:

 1     var SingletonCreateDiv = (function(){
 2         var instance;
 3         return function( html ){
 4             if ( !instance ){
 5                 instance = new CreateDiv( html );
 6             }
 7             return instance;
 8         }
 9     })();
10     var a = new SingletonCreateDiv( 'sven1' );
11     var b = new SingletonCreateDiv( 'sven2' );
12     alert ( a === b );     //true

通过引入代理类的方式,完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类SingletonCreateDiv中。这样一来,CreateDiv就变成了一个普通的类,它跟SingletonCreateDiv组合起来可以达到单例模式的效果。本例是缓存代理的应用之一,这样写的好处是毋庸置疑的。

4.JavaScript中的单例模式

前面几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从“类”中创建而来。在以类为中心的语言中,这是很自然的做法,对象总是从类中创建而来的。而在JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在JavaScript中并不适用。

单例模式的核心是确保只有一个实例,并提供全局访问。

全局变量不是单例模式,但在JavaScript 开发中,我们经常会把全局变量当成单例来使用。

var a = {};

当用这种方式创建对象a 时,对象a 确实是独一无二的。如果a变量被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样就满足了单例模式的两个条件。

但这样明显会污染全局的命名空间,有这样几种方式可以相对降低其它全局变量带来的命名污染:

  (1)使用命名空间

  变量放到了命名空间内,成为了命名空间的属性

1     var namespace1 = {
2         a: function(){
3             alert (1);
4         },
5         b: function(){
6             alert (2);
7         }
8     };

  (2)使用闭包封装私有变量

  这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信。

1     var user = (function(){
2         var __name = 'sven',
3         __age = 29;
4         return {
5             getUserInfo: function(){
6                 return __name + '-' + __age;
7             }
8         }
9     })();

5.惰性单例

惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实际开发中非常有用,有用的程度可能超出了我们的想象。

首先我们想要创建一个悬浮窗用于登录,被一个点击事件触发,且悬浮窗唯一:

 1     var loginLayer = (function(){     //loginLayer就是单例对象,这里没有用类的方式创建,而是直接给了一个全局对象
 2         var div = document.createElement( 'div' );
 3         div.innerHTML = '我是登录浮窗';
 4         div.style.display = 'none';
 5         document.body.appendChild( div );
 6         return div;
 7     })();
 8 
 9     document.getElementById( 'loginBtn' ).onclick = function(){
10         loginLayer.style.display = 'block';
11     };

这个方法缺点很明显,如果我们一直不去登录,由于这个悬浮窗是早就创建好的,这样就有可能浪费一个DOM节点。应该用户点击之后才创建:

 1     var createLoginLayer = function(){
 2         var div = document.createElement( 'div' );
 3         div.innerHTML = '我是登录浮窗';
 4         div.style.display = 'none';
 5         document.body.appendChild( div );
 6         return div;
 7     };
 8     document.getElementById( 'loginBtn' ).onclick = function(){
 9         var loginLayer = createLoginLayer();
10         loginLayer.style.display = 'block';
11     };

这次达到了惰性的目的,但是失去了单例效果,每次点击都会创建一个悬浮窗。我们可以用一个变量来判断是否已经创建过登录浮窗:

 1     var createLoginLayer = (function(){   //自执行创建闭包,保存实例
 2         var div;    //实例
 3         return function(){
 4             if ( !div ){  //如果实例不存在,创建实例
 5                 div = document.createElement( 'div' );
 6                 div.innerHTML = '我是登录浮窗';
 7                 div.style.display = 'none';
 8                 document.body.appendChild( div );
 9             }
10             return div;   //返回实例
11         }
12     })();
13 
14     document.getElementById( 'loginBtn' ).onclick = function(){
15         var loginLayer = createLoginLayer();
16         loginLayer.style.display = 'block';
17     };

这次代码虽然实现了功能,但是又违反了之前提过的“单一职责原则”,我们需要把管理单例的代码抽离出来:

 1     var getSingle = function( fn ){   //管理单例,fn为创建一个对象的函数
 2         var result;
 3         return function(){
 4             return result || ( result = fn .apply(this, arguments ) );
 5         }
 6     };
 7 
 8     var createLoginLayer = function(){     //创建悬浮窗
 9         var div = document.createElement( 'div' );
10         div.innerHTML = '我是登录浮窗';
11         div.style.display = 'none';
12         document.body.appendChild( div );
13         return div;
14     };
15 
16     var createSingleLoginLayer = getSingle( createLoginLayer );    //创建一个单例悬浮窗函数
17 
18     document.getElementById( 'loginBtn' ).onclick = function(){
19         var loginLayer = createSingleLoginLayer();    //调用创建单例的函数
20         loginLayer.style.display = 'block';
21     };

利用单例模式还可以完成事件代理,只绑定一次事件

 1     var bindEvent = getSingle(function(){
 2         document.getElementById( 'div1' ).onclick = function(e){
 3             alert ( 'e.target' );
 4         }
 5         return true;  //单例需要接收一个返回值
 6     });
 7     var render = function(){
 8         console.log( '开始渲染列表' );
 9         bindEvent();
10     };
11 
12     render();   //这里即使运行了三次,但只绑定了一次,不会浪费性能
13     render();
14     render();

总结

单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。将管理单例和创建对象的方法分开是种很好的思路。

原文地址:https://www.cnblogs.com/grey-zhou/p/6062759.html