zoukankan      html  css  js  c++  java
  • 《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();

    总结

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

  • 相关阅读:
    Java 8 Lambda 表达式
    OSGi 系列(十二)之 Http Service
    OSGi 系列(十三)之 Configuration Admin Service
    OSGi 系列(十四)之 Event Admin Service
    OSGi 系列(十六)之 JDBC Service
    OSGi 系列(十)之 Blueprint
    OSGi 系列(七)之服务的监听、跟踪、声明等
    OSGi 系列(六)之服务的使用
    OSGi 系列(三)之 bundle 事件监听
    OSGi 系列(三)之 bundle 详解
  • 原文地址:https://www.cnblogs.com/grey-zhou/p/6062759.html
Copyright © 2011-2022 走看看