定义

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

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

1.实现单例模式

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

 var Singleton = function( name ){  //构造函数
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function(){ //构造器原形上添加方法,可以获得对象的name属性
alert ( this.name );
};
Singleton.getInstance = function( name ){
if ( !this.instance ){ //如果不存在对象实例
this.instance = new Singleton( name ); //创建对象实例
}
return this.instance; //返回对象实例
};
var a = Singleton.getInstance( 'sven1' );
var b = Singleton.getInstance( 'sven2' );
alert ( a === b ); // true
console.log(a.name); // sven1
console.log(b.name); // seven1
console.log(a.instance); // null
console.log(b.instance); // null
console.log(window.instance); // Singleton {name: "sven1", instance: null}

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

     var Singleton = function( name ){  //构造函数
this.name = name;
this.instance = null; //无效的一个属性
};
Singleton.prototype.getName = function(){ //构造器原型上添加方法,可以获得对象的name属性
alert ( this.name );
};
create = function( name ){ //全局创建对象的函数
if ( !this.sing ){ //这里的this指向的是window,即全局,如果全局不存在sing对象
this.sing = new Singleton( name ); //创建sing对象
}
return this.sing; //返回sing对象
};
var a = create( 'sven1' );
var b = create( 'sven2' );
alert ( a === b ); // true
console.log(a.name); // sven1
console.log(b.name); // seven1
console.log(a.instance); // null
console.log(b.instance); // null
console.log(window.sing); // Singleton {name: "sven1", instance: null}

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

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

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

2.改进单例模式

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

     var CreateDiv = (function(){   //匿名自执行函数,同时返回一个函数,创造了一个闭包环境
var instance; //利用闭包存储的对象实例
var CreateDiv = function( html ){ //返回的函数
if ( instance ){ //如果对象存在,返回对象
return instance;
}
this.html = html; //不存在就赋值,创建
this.init();
return instance = this; //new的时候返回实例
};
CreateDiv.prototype.init = function(){ //原型上绑定的方法
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};
return CreateDiv;
})(); var a = new CreateDiv( 'sven1' );
var b = new CreateDiv( 'sven2' );
alert ( a === b ); // true

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

     var CreateDiv = function( html ){
if ( instance ){
return instance;
}
this.html = html;
this.init();
return instance = this;
};

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

3.用代理实现单例模式

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

     var CreateDiv = function( html ){
this.html = html;
this.init();
};
CreateDiv.prototype.init = function(){
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};

接下来引入代理类SingletonCreateDiv:

     var SingletonCreateDiv = (function(){
var instance;
return function( html ){
if ( !instance ){
instance = new CreateDiv( html );
}
return instance;
}
})();
var a = new SingletonCreateDiv( 'sven1' );
var b = new SingletonCreateDiv( 'sven2' );
alert ( a === b ); //true

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

4.JavaScript中的单例模式

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

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

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

var a = {};

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

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

  (1)使用命名空间

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

     var namespace1 = {
a: function(){
alert (1);
},
b: function(){
alert (2);
}
};

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

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

     var user = (function(){
var __name = 'sven',
__age = 29;
return {
getUserInfo: function(){
return __name + '-' + __age;
}
}
})();

5.惰性单例

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

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

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

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

     var createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
};
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};

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

     var createLoginLayer = (function(){   //自执行创建闭包,保存实例
var div; //实例
return function(){
if ( !div ){ //如果实例不存在,创建实例
div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
}
return div; //返回实例
}
})(); document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};

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

     var getSingle = function( fn ){   //管理单例,fn为创建一个对象的函数
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
}; var createLoginLayer = function(){ //创建悬浮窗
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
}; var createSingleLoginLayer = getSingle( createLoginLayer ); //创建一个单例悬浮窗函数 document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer(); //调用创建单例的函数
loginLayer.style.display = 'block';
};

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

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

总结

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

javascript设计模式与开发实践阅读笔记(4)——单例模式的更多相关文章

  1. javascript设计模式与开发实践阅读笔记(8)——观察者模式

    发布-订阅模式,也叫观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 在JavaScript开发中,我们一般用事件模型来替代传统的观察者模式. ...

  2. javascript设计模式与开发实践阅读笔记(7)——迭代器模式

    迭代器模式:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示. 迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺 ...

  3. javascript设计模式与开发实践阅读笔记(6)——代理模式

    代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问. 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对 ...

  4. javascript设计模式与开发实践阅读笔记(5)——策略模式

    策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换. 我的理解就是把各种方法封装成函数,同时存在一个可以调用这些方法的公共函数.这样做的好处是可以消化掉内部的分支判断,使代码效率 ...

  5. javascript设计模式与开发实践阅读笔记(9)——命令模式

    命令模式:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系. 说法很复 ...

  6. javascript设计模式与开发实践阅读笔记(11)—— 模板方法模式

    模板方法模式: 由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类.通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序.子类通过继承这个抽象类,也继 ...

  7. JavaScript设计模式与开发实践——读书笔记1.高阶函数(上)

    说来惭愧,4个多月未更新了.4月份以后就开始忙起来了,论文.毕设.毕业旅行等七七八八的事情占据了很多时间,毕业之后开始忙碌的工作,这期间一直想写博客,但是一直没能静下心写.这段时间在看<Java ...

  8. 《JavaScript设计模式与开发实践》笔记第八章 发布-订阅模式

    第八章 发布-订阅模式 发布-订阅模式描述 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 发布-订阅模式可以广泛应用于 ...

  9. JavaScript设计模式与开发实践——读书笔记1.高阶函数(下)

    上部分主要介绍高阶函数的常见形式,本部分将着重介绍高阶函数的高级应用. 1.currying currying指的是函数柯里化,又称部分求值.一个currying的函数会先接受一些参数,但不立即求值, ...

随机推荐

  1. C# 代码页获取input的值

    <input id="aa" name="iaa" type="text" /> Label1.Text = Request.F ...

  2. 初步认识JUnit

    初步认识JUnit 目前大多数的基于Java的企业应用软件,肯定少不了单元测试,程序员通过编写单元测试来验证自己程序的有效性:管理者通过持续自动的执行单元测试和分析单元测试覆盖率来确保软件本身的质量. ...

  3. 关于C# 窗体自动隐藏和加载的问题

    最近在写一个小项目,开发一个小程序配合其他软件使用,其中一款软件在使用工作时需要截图生成报告,此时不能有其他应用程式界面在显示器桌面显示,故需要自动隐藏和加载窗体,通过阅读Windows API实现了 ...

  4. CardView的简单介绍

    CardView是Android5.0中的一个全新控件,本质上而言,CardView是一个增加了圆角和阴影效果的FrameLayout,没错它就是一个FrameLayout,一个布局.CardView ...

  5. 数据库的char(n)

    Mysql中的char(n)或者varchar(n) 其中的n就是代表列,不代表字节! varchar(n)其中的n最多是65535 , 应该在创建表的同时,指定表的编码方式为latin1,因为lat ...

  6. c# (nop中)下拉列表(有外键)

    第一种情况.view视图加载出来时就有值,实现步骤如下 1.在操作的界面Model中建立public List<SelectListItem> xxx(取名){ get; set; } 2 ...

  7. hd2066一个人的旅行

    Problem Description 虽然草儿是个路痴(就是在杭电待了一年多,居然还会在校园里迷路的人,汗~),但是草儿仍然很喜欢旅行,因为在旅途中 会遇见很多人(白马王子,^0^),很多事,还能丰 ...

  8. 获得select下拉框的值

    html -------------------------------------------------------------------------------------- <sele ...

  9. [转]MySQL主从复制原理介绍

    MySQL主从复制原理介绍 一.复制的原理 MySQL 复制基于主服务器在二进制日志中跟踪所有对数据库的更改(更新.删除等等).每个从服务器从主服务器接收主服务器已经记录到其二进制日志的保存的更新,以 ...

  10. xtrabackup工具

    xtrabackup是基于InnoDB存储引擎灾难恢复的.它复制InnoDB的数据文件,尽管数据文件在内部是非一致性的,但在执行灾难恢复时可以保证这些数据文件是一致的,并且可用. 官方原理 在Inno ...