javascript设计模式与开发实践阅读笔记(4)——单例模式
定义
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
具体来说,就是保证有些对象有且只有一个,比如线程池、全局缓存、浏览器中的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)——单例模式的更多相关文章
- javascript设计模式与开发实践阅读笔记(8)——观察者模式
发布-订阅模式,也叫观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 在JavaScript开发中,我们一般用事件模型来替代传统的观察者模式. ...
- javascript设计模式与开发实践阅读笔记(7)——迭代器模式
迭代器模式:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示. 迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺 ...
- javascript设计模式与开发实践阅读笔记(6)——代理模式
代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问. 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对 ...
- javascript设计模式与开发实践阅读笔记(5)——策略模式
策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换. 我的理解就是把各种方法封装成函数,同时存在一个可以调用这些方法的公共函数.这样做的好处是可以消化掉内部的分支判断,使代码效率 ...
- javascript设计模式与开发实践阅读笔记(9)——命令模式
命令模式:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系. 说法很复 ...
- javascript设计模式与开发实践阅读笔记(11)—— 模板方法模式
模板方法模式: 由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类.通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序.子类通过继承这个抽象类,也继 ...
- JavaScript设计模式与开发实践——读书笔记1.高阶函数(上)
说来惭愧,4个多月未更新了.4月份以后就开始忙起来了,论文.毕设.毕业旅行等七七八八的事情占据了很多时间,毕业之后开始忙碌的工作,这期间一直想写博客,但是一直没能静下心写.这段时间在看<Java ...
- 《JavaScript设计模式与开发实践》笔记第八章 发布-订阅模式
第八章 发布-订阅模式 发布-订阅模式描述 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 发布-订阅模式可以广泛应用于 ...
- JavaScript设计模式与开发实践——读书笔记1.高阶函数(下)
上部分主要介绍高阶函数的常见形式,本部分将着重介绍高阶函数的高级应用. 1.currying currying指的是函数柯里化,又称部分求值.一个currying的函数会先接受一些参数,但不立即求值, ...
随机推荐
- plupload简易应用 多图片上传显示预览以及删除
<script> var uploader = new plupload.Uploader({ //实例化一个plupload上传对象 browse_button: 'btnBrowse' ...
- ABAP 根据操作员分组发送邮件
1,获取操作员姓名 SELECT SINGLE ADRP~NAME_TEXT INTO GS_OUTPUT-UNAMT FROM ADRP INNER JOIN USR21 ON ADRP~PERSN ...
- PostGreSQL 分页
select * from users limit 10 offset 20; limit A offset B 其中A是页容量 B是偏移量 即跳过前20条 查询每页10条
- XE3随笔8:关于乱码
以下例子都会出现乱码, 虽然都可以有变通的方案, 但如果不乱码就太好了! unit Unit1; interface uses Windows, Messages, SysUtils, Varia ...
- [python] 线程池
特别感谢simomo 什么是线程池? 诸如web服务器.数据库服务器.文件服务器和邮件服务器等许多服务器应用都面向处理来自某些远程来源的大量短小的任务.构建服务器应用程序的一个过于简单的模型是:每当一 ...
- 20145225《Java程序设计》 第8周学习总结
20145225<Java程序设计> 第八周学习总结 教材学习内容总结 第十五章 通用API 15.1日志 日志API:使用日志的起点是Logger类,要取得Logger类,必须使用Log ...
- 下载python标准库--python
#coding:utf-8 import urllib2 import os,sys from BeautifulSoup import BeautifulSoup # For processing ...
- javascript的原型和继承(1)
原型与继承是javascript中基础,重要而相对比较晦涩难解的内容.在图灵的网上看到一篇翻译过的文章,有参考了一些知名博客.我自己总结了几篇.通过这次的总结,感觉自己对原型和继承的认识又增加了很多, ...
- ubuntu 启动项创建器 选择不了CD镜像,IOS镜像的解决方法
自己系统是ubuntu14.04 , 想使用 ubuntu自带的启动项创建器(usb-creator-gtk)做一个CDLinux的U盘启动项, 打开程序后发现U盘识别了, 在添加镜像的时候,发现怎么 ...
- alert()、confirm()和prompt()的区别
1.警告消息框alertalert 方法有一个参数,即希望对用户显示的文本字符串.该字符串不是 HTML 格式.该消息框提供了一个“确定”按钮让用户关闭该消息框,并且该消息框是模式对话框,也就是说,用 ...