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();

    总结

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

  • 相关阅读:
    【如何入门ACM】
    HDU
    HDU 6107 Typesetting
    bzoj 3223: Tyvj 1729 文艺平衡树
    51Nod 1781 跑的比谁都快
    51Nod 1331 狭窄的通道
    51Nod 1555 布丁怪
    hihocoder 1035 : 自驾旅行 III
    51Nod 1196 字符串的数量
    51Nod 1530 稳定方块
  • 原文地址:https://www.cnblogs.com/grey-zhou/p/6062759.html
Copyright © 2011-2022 走看看