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),实际上在它的文件中包含着很多这样类似的代码,比如从文件一开头就可以发现:

ContractedBlock.gif ExpandedBlockStart.gif Code
var Prototype = {
  Version: 
'1.6.0.3'

  Browser: {
    IE:     
!!(window.attachEvent &&
      navigator.userAgent.indexOf(
'Opera'=== -1),
    Opera:  navigator.userAgent.indexOf(
'Opera'> -1,
    WebKit: navigator.userAgent.indexOf(
'AppleWebKit/'> -1,
    Gecko:  navigator.userAgent.indexOf(
'Gecko'> -1 &&
      navigator.userAgent.indexOf(
'KHTML'=== -1,
    MobileSafari: 
!!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  }, 

  BrowserFeatures: {
    XPath: 
!!document.evaluate,
    SelectorsAPI: 
!!document.querySelector,
    ElementExtensions: 
!!window.HTMLElement,
    SpecificElementExtensions:
      document.createElement(
'div')['__proto__'&&
      document.createElement(
'div')['__proto__'!==
        document.createElement(
'form')['__proto__']
  }, 

  ScriptFragment: 
'<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: 
/^\/\*-secure-([\s\S]*)\*\/\s*$/

  emptyFunction: 
function() { },
  K: 
function(x) { return x }
};
//以下是它的使用
if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions 
= false;

可以看出,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文件中也用到命名空间的单件模式:

ContractedBlock.gif ExpandedBlockStart.gif Code
var Class = {
  create: 
function() {
    
var parent = null, properties = $A(arguments);
    
if (Object.isFunction(properties[0]))
      parent 
= properties.shift();

    
function klass() {
      
this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass 
= parent;
    klass.subclasses 
= [];

    
if (parent) {
      
var subclass = function() { };
      subclass.prototype 
= parent.prototype;
      klass.prototype 
= new subclass;
      parent.subclasses.push(klass);
    }

    
for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    
if (!klass.prototype.initialize)
      klass.prototype.initialize 
= Prototype.emptyFunction;

    klass.prototype.constructor 
= klass;

    
return klass;
  }
};

Class.Methods 
= {
  addMethods: 
function(source) {
    
var ancestor   = this.superclass && this.superclass.prototype;
    
var properties = Object.keys(source);

    
if (!Object.keys({ toString: true }).length)
      properties.push(
"toString""valueOf");

    
for (var i = 0, length = properties.length; i < length; i++) {
      
var property = properties[i], value = source[property];
      
if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() 
== "$super") {
        
var method = value;
        value 
= (function(m) {
          
return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method);

        value.valueOf 
= method.valueOf.bind(method);
        value.toString 
= method.toString.bind(method);
      }
      
this.prototype[property] = value;
    }

    
return this;
  }
};

这里实际上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文件中,其实也用到闭包方式的单件模式:

ContractedBlock.gif ExpandedBlockStart.gif Code
var Hash = Class.create(Enumerable, (function() {

  
function toQueryPair(key, value) {
    
if (Object.isUndefined(value)) return key;
    
return key + '=' + encodeURIComponent(String.interpret(value));
  }

  
return {
    initialize: 
function(object) {
      
this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

    _each: 
function(iterator) {
      
for (var key in this._object) {
        
var value = this._object[key], pair = [key, value];
        pair.key 
= key;
        pair.value 
= value;
        iterator(pair);
      }
    },

    set: 
function(key, value) {
      
return this._object[key] = value;
    },

    get: 
function(key) {
      
// simulating poorly supported hasOwnProperty
      if (this._object[key] !== Object.prototype[key])
        
return this._object[key];
    },

    unset: 
function(key) {
      
var value = this._object[key];
      
delete this._object[key];
      
return value;
    },

    toObject: 
function() {
      
return Object.clone(this._object);
    },

    keys: 
function() {
      
return this.pluck('key');
    },

    values: 
function() {
      
return this.pluck('value');
    },

    index: 
function(value) {
      
var match = this.detect(function(pair) {
        
return pair.value === value;
      });
      
return match && match.key;
    },

    merge: 
function(object) {
      
return this.clone().update(object);
    },

    update: 
function(object) {
      
return new Hash(object).inject(thisfunction(result, pair) {
        result.set(pair.key, pair.value);
        
return result;
      });
    },

    toQueryString: 
function() {
      
return this.inject([], function(results, pair) {
        
var key = encodeURIComponent(pair.key), values = pair.value;

        
if (values && typeof values == 'object') {
          
if (Object.isArray(values))
            
return results.concat(values.map(toQueryPair.curry(key)));
        } 
else results.push(toQueryPair(key, values));
        
return results;
      }).join(
'&');
    },

    inspect: 
function() {
      
return '#<Hash:{' + this.map(function(pair) {
        
return pair.map(Object.inspect).join('');
      }).join(
''+ '}>';
    },

    toJSON: 
function() {
      
return Object.toJSON(this.toObject());
    },

    clone: 
function() {
      
return new Hash(this);
    }
  }
})());

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) - 面向对象基础以及接口和继承类的实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值