• Javascript乱弹设计模式系列(6) - 单件模式(Singleton)


    前言

    博客园谈设计模式的文章很多,我也受益匪浅,包括TerryLee吕震宇等等的.NET设计模式系列文章,强烈推荐。对于我,擅长于前台代码的开发,对于设计模式也有一定的了解,于是我想结合Javascript来设计前台方面的“设计模式”,以对后台“设计模式”做个补充。开始这个系列我也诚惶诚恐,怕自己写得不好,不过我也想做个尝试,一来希望能给一些人有些帮助吧,二来从写文章中锻炼下自己,三来通过写文章对自己增加自信;如果写得不好,欢迎拍砖,我会虚心向博客园高手牛人们学习请教;如果觉得写得还可以,谢谢大家的支持了:)

    这篇主要对于单件模式的各种代码形式进行归纳总结。可能大家没有经意,实际上,单件模式也许是我们日常前端JS开发中使用频率最高的设计模式了。

     

    概述

    在软件系统中,总有一些类只能或者必须产生一个实例对象,比如线程池,缓存,注册表等等;对于这种类,如果产生多个实例对象,就会出现各种异常状况;对于这种对象只要创建一次并且分配一次内存空间即可,所以这里也有个问题,对象所分配的内存空间的消耗,对于长期不使用的对象,这就产生资源浪费,所以利用单件模式,也可以按照需要来创建对象。

     

    定义

    单件模式确保一个类只有一个实例,并且提供一个全局访问点。 

    类图

    分析

    首先我们考虑到传统的编程语言如C#,单件模式中设置一个静态变量,可以这样表示:

    public sealed class Singleton
    {
        
    static Singleton instance = null;
        
    //
    }

    Singleton的构造函数设置为私有,防止Singleton多次实例化,这样就可以有以下的静态方法:

    public static Singleton Instance
    {
        
    get
        {
            
    if (instance == null)
            {
                instance 
    = new Singleton();
            }
            
    return instance;
        }
    }

    这样只有在第一次实例化的时候,才创建对象;通过静态方法,得到唯一实例;

    这个是C#中最简单的单件模式写法。而Javascript作为弱类型语言,有着它独特的地方,现在我就来介绍Javascript单件模式的几种形式:

    1. 最基本的单件模式

    var LoginUser = {
        name : 
    "匿名用户",
        sex : 
    "保密",
        setName : 
    function(name){
            
    this.name = name;
        },
        setSex : 
    function(sex){
            
    this.sex = sex;
        },
        getUserInfo : 
    function() {
            
    return "用户名:" + this.name + ";性别:" +this.sex;
        }
    }

    这里定义了一个对象(LoginUser),对象中包含了各种属性(name,sex)和方法(setName,setSex,getUserInfo);

    这样我新建一个HTML页面:

    <script type="text/javascript">
    //
    window.onload = function() {
        alert(LoginUser.getUserInfo());
        LoginUser.setName(
    "Leepy");
        LoginUser.setSex(
    "");
        
    //alert(LoginUser.getUserInfo());
    }
    function test() {
        alert(LoginUser.getUserInfo());
    }
    </script>
    <input type="button" value="test" onclick="test();" />

    可以发现,界面初始化时弹出的警告框为“用户名:匿名用户;性别:保密”,通过setName和setSex方法之后,点击按钮后弹出的警告框为“用户名:Leepy;性别:男”,说明在不同的方法作用域下,LoginUser保持着修改后的状态,因此LoginUser在页面中就保持着单一的状态。


    我想大家一定也听过prototype的JS框架了吧(http://www.prototypejs.org),最新版本为(http://www.prototypejs.org/assets/2008/9/29/prototype-1.6.0.3.js),实际上在它的文件中包含着很多这样类似的代码,比如从文件一开头就可以发现:

    Code

    可以看出,Version、Browser、BrowserFeatures、ScriptFragment、JSONFilter作为Prototype对象的属性,而emptyFUnction、K作为Prototype对象的方法;因此它就是一个最基本的单件模式。

    由于Javascript的语言特性,可以在后期动态添加,删除,修改属性:

    如:LoginUser.age = 24;  那么LoginUser对象便增加了age属性;而如:delete LoginUser.name; 那么LoginUser对象就删除了name属性;而如:LoginUser.name = "cnblogs"; 那么LoginUser对象的“私有”属性不需要通过setName的“公有”方法仍然能够做出修改。

    因为根据设计模式原则:对扩展开放而对修改关闭,显然违背了该条准则。为了防止这种情况的发生,到时候会引入闭包的方式,稍后会说明。

    2. 命名空间的单件模式

    命名空间可以很好地划分 属性和方法 的归属,以及可以防止 属性和方法 被轻易的修改,通过访问各自的命名空间得到对应我们想要的 属性和方法。这里还是以上面的LoginUser为例:

    var LoginUser = {
        name : 
    "匿名用户",
        sex : 
    "保密",
        setName : 
    function(name){
            
    this.name = name;
        },
        setSex : 
    function(sex){
            
    this.sex = sex;
        },
        getUserInfo : 
    function() {
            
    return "用户名:" + this.name + ";性别:" +this.sex;
        }
    }
    LoginUser.Mother 
    = {
        name : 
    "母亲姓名",
        career : 
    "职位",
        setName : 
    function(name){
            
    this.name = name;
        },
        setCareer : 
    function(career){
            
    this.career = career;
        },
        getUserInfo : 
    function() {
            
    return LoginUser.name + "的母亲名字:" + this.name + ";职业:" +this.career;
        }
    }

    从代码中看出,这里我把LoginUser作为“命名空间”,而LoginUser.Mother作为它的一个“全局变量”,这样做的好处可以防止LoginUser的属性和方法被轻易地覆盖,通过LoginUser.××××,以致于LoginUser.××××.××××(如LoginUser.Mother.Brother)来划分 属性和方法 的归属,如LoginUser中的name属性和LoginUser.Mother中的name属性是区分开来的。

    这样我新建一个HTML页面:

    <script type="text/javascript"> 
    // 
    window.onload = function() {
        LoginUser.setName(
    "Leepy");
        LoginUser.Mother.setName(
    "admin");
        LoginUser.Mother.setCareer(
    "农民");
        alert(LoginUser.Mother.getUserInfo());
    }
    </script>

    可以得到下面的弹出框:

     LoginUser和LoginUser.Mother的name属性已经区分开来了。


    在prototype.js文件中也用到命名空间的单件模式:

    Code

    这里实际上Class.create实现的是类的继承,具体这里我就不再阐述了,大家可以查看prototype官方的Api文档。


    3. 闭包方式的单件模式

    如果要得到真正意义上的“私有”成员,那么闭包方式是构造单件模式的一种选择。通过闭包的方式,只暴露一些可以公开的方法或者属性,而私有成员只在内部实现操作,而所有的属性和方法只需要实例化一次。现在开始继续看LoginUser的例子,闭包方式单件模式(左)对比第1条基本单件模式(右)的例子:

    var LoginUser = (function(){
        
    var _name = "匿名用户";
        
    var _sex = "保密";
        
    return {
            setName : 
    function(name){
                _name 
    = name;
            },
            setSex : 
    function(sex){
                _sex 
    = sex;
            },
            getUserInfo : 
    function(){
                
    return "用户名:" + _name + ";性别:" + _sex;
            },
            getName : 
    function(){
                
    return _name;
            }
        };
    })();
    var LoginUser = {
        _name : 
    "匿名用户",
        _sex : 
    "保密",
        setName : 
    function(name){
            
    this._name = name;
        },
        setSex : 
    function(sex){
            
    this._sex = sex;
        },
        getUserInfo : 
    function() {
            
    return "用户名:" + this._name + ";性别:" +this._sex;
        }
    }

    可以发现,闭包方式将公共的方法放在return { ... }中,而属性_name和_sex做为参数传入return { ... }中;

    现在两种方式都实现一下代码,测试一下:

    window.onload = function() {
        LoginUser.setName(
    "Leepy");
        LoginUser.setSex(
    "");
        alert(LoginUser._name);
    }

    可以得到闭包方式单件模式(左)对比第1条基本单件模式(右)如下两个结果:

    可以看出闭包方式的LoginUser无法得到_name的值,而基本方式的LoginUser可以得到_name的值;

    这进一步说明了闭包方式的_name已经成为“私有”成员属性了。而如果要得到_name的值,只有通过公开方法或者公开属性来获得,如下:

    return { // 注意这里的“{”号不能够换行到下一行,不然浏览器提示错误
        getName : function() {
           
    return _name;
       }
    }

    这样子,alert(LoginUser.getName()); 就可以显示正确的值了。


    继续看prototype.js文件中,其实也用到闭包方式的单件模式:

    Code

    Class.create的第二个参数就是闭包方式的单件对象,这里的作用是将Hash对象继承于Enumerable类,并且包含了单件对象的公开方法如set,get,keys,values等等操作获取散列键值的方法。具体这里我就不再阐述了,大家可以查看prototype官方的Api文档。

    4. 延迟加载的单件模式

    上面介绍的各种方式都是在建立对象的时候,对象内部的成员都已经加载完毕,如果对于资源占用多的脚本,在不需要的时候,这对于内存造成了极大的浪费,所以要考虑一种方式将成员实例化推迟到需要调用对象的时候,也就是叫做延迟加载

    继续以LoginUser的例子作为演示:

    var LoginUser = (function(){
        
    var uniqueInstance;
        
    var _name;
        
    var _sex;
        
    function constructor(){
            _name 
    = "匿名用户";
            _sex 
    = "保密";
            
            
    return {
                setName : 
    function(name){
                _name 
    = name;
                },
                setSex : 
    function(sex){
                    _sex 
    = sex;
                },
                getUserInfo : 
    function(){
                    
    return "用户名:" + _name + ";性别:" + _sex;
                },
                getName : 
    function(){
                    
    return _name;
                }
            };
        }
        
        
    return {
            getInstance : 
    function() {
                
    if(uniqueInstance == null)
                {
                    uniqueInstance 
    = constructor();
                }
                
    return uniqueInstance;
            }
        }
    })();

    可以看到,我这里添加了一个私有方法constructor(),并且由它来公开成员方法;
    再则,当第一次调用getInstance方法的时候,调用contructor方法,并且对于成员属性_name和_sex进行初始化,说明通过调用getInstance方法才对属性进行初始化,平时不进行初始化,constructor()返回了一个uniqueInstance的对象,由uniqueInstance对象负责该单件对象的公开方法的操作。

    然后新建一个HTML页面:

    window.onload = function() {
        
    var user1 = LoginUser.getInstance();
        user1.setName(
    "Leepy");
        user1.setSex(
    "");
        alert(user1.getUserInfo());
    //用户名:Leepy;性别:男
        
        
    var user2 = LoginUser.getInstance();
        alert(user1 
    == user2); //user1和user2共享同一块内存空间,为true
    }

    只有通过LoginUser.getInstance()后,LoginUser才进行成员初始化,而方法返回的对象共享同一块的内存空间。这就是延迟加载单件模式的工作原理。

    5. 其他

    另外上次园里一位朋友(winter-cn)发我的一个单件设计模式的文章,觉得挺不错,它也是利用了“匿名”函数的特征构建了单件模式,我这里把代码贴出来一下:

    <script>
    (
    function(){
        
    //instance declared
        //SingletonFactory Interface
        SingletonFactory = {
            getInstance : getInstance
        }

        
    //private classes
        function SingletonObject()
        {
            SingletonObject.prototype.methodA 
    = function()
            {
                alert(
    'methodA');
            }
            SingletonObject.prototype.methodB 
    = function()
            {
                alert(
    'methodB');
            }
            SingletonObject.instance 
    = this;
        }
        
        
    //SingletonFactory implementions
        function getInstance()
        {
            
    if(SingletonObject.instance == null)
                
    return new SingletonObject();
                
            
    else
                
    return SingletonObject.instance;
        }

    })();

    var instA = null;
    try
    {
    alert(
    "试图通过new SingletonObject()构造实例!");
    instA 
    = new SingletonObject();
    }
    catch(e){alert("SingletonObject构造函数不能从外部访问,系统抛出了异常!");}

    instA 
    = SingletonFactory.getInstance();  //通过Factory上定义的静态方法获得
    var instB = SingletonFactory.getInstance();
    instA.methodA();
    instB.methodA();

    alert(instA 
    == instB); //成功

    var instC = null;
    try
    {
    alert(
    "试图通过new SingletonObject()构造实例!");
    instC 
    = new SingletonObject();
    }
    catch(e){alert("SingletonObject构造函数不能从外部访问,系统抛出了异常!");}
    </script>

    总结

    该篇文章用Javascript来设计单件模式的几种方式,在许多优秀的JS开源框架中,如prototype,MicrosoftAjaxLibrary,jQuery,MooTools等等,都包含着大量单件模式的应用,所以单件模式在Javascript中是很重要的一个模式。

    附:源代码下载

    本篇到此为止,谢谢大家阅读

    参考文献:《Head First Design Pattern》

    《Professional Javascript Design Patterns》

    本系列文章转载时请注明出处,谢谢合作!

     相关系列文章:
    Javascript乱弹设计模式系列(6) - 单件模式(Singleton)
    Javascript乱弹设计模式系列(5) - 命令模式(Command)
    Javascript乱弹设计模式系列(4) - 组合模式(Composite)
    Javascript乱弹设计模式系列(3) - 装饰者模式(Decorator)
    Javascript乱弹设计模式系列(2) - 抽象工厂以及工厂方法模式(Factory)
    Javascript乱弹设计模式系列(1) - 观察者模式(Observer)
    Javascript乱弹设计模式系列(0) - 面向对象基础以及接口和继承类的实现

  • 相关阅读:
    CustomDrawableTextView
    Snippet: align a TextView around an image
    How to import library ?
    Gradle自定义你的BuildConfig
    使用adb shell dumpsys检测Android的Activity任务栈
    Activity intent经常使用的 FLAG
    使用 ContentProviderOperation 来提升性能
    幻方算法
    自己制作的粉碎机批处理程序
    Ubuntu 16.10 server 相关
  • 原文地址:https://www.cnblogs.com/liping13599168/p/1384991.html
Copyright © 2020-2023  润新知