单例模式的定义是:javascript
保证一个类仅有一个实例,并提供一个访问它的全局访问点。html
单例模式是一种经常使用的模式,有一些对象咱们每每只须要一个,好比线程池、全局缓存、浏览器的window对象等。例如,当咱们点击登陆按钮时,页面会弹出一个登陆悬浮窗,而这个登陆悬浮窗是惟一的,不管点击多少次登陆按钮,这个悬浮窗只会被建立一次,这时,这个悬浮窗就适合用单例模式来建立。java
实现一个标准的单例模式,通常是用一个变量来标志当前是否已经为某个类建立过对象,如果,则在下一次获取该类的实例时,直接返回以前建立的对象。web
var Singleton = function(name){ this.name = name; this.instance = null; } Singleton.prototype.getName = function(){ console.log(this.name); }; Singleton.getInstance = function(name){ if(! this.instance){ this.instance = new Singleton(name); } return this.instance; }; var a = Singleton.getInstance('sin1'); var b = Singleton.getInstance('sin2'); console.log(a === b); // 输出:true
咱们经过Singleton.getInstance来获取Singleton类的惟一对象,这种方式想对简单,但有一个问题,就是增长了类的“不透明性”,Singleton类的使用者必须知道这是一个单例类,跟以往经过new xxx来获取对象的方式不一样,这里只能使用Singleton.getInstance来获取对象。ajax
如今咱们经过一段代码来实现一个透明的单例类,用户从这个类中建立对象的时候,能够像使用其余任何普通类同样。编程
var createDiv = (function(){ var instance; var createDiv = function(html){ if(instance){ return instance; } this.html = html; this.init(); return instance = this; }; createDiv.prototype.init = function(){ var div = document.createElement('div'); div.innerHTML = this.html; document.body.appendChild(div); }; return createDiv; })(); var a = new createDiv('sin1'); var b = new createDiv('sin2'); console.log(a === b); // 输出:true
为了把instance封装起来,咱们使用了自执行的匿名函数和闭包,而且让这个匿名函数返回真正的Singleton构造方法,这增长了一些程序的复杂度,阅读起来也不是很舒服。设计模式
观察Singleton构造函数的代码,该构造函数实际上负责了两件事情:第一是建立对象和执行初始化init方法,第二是保证只有一个对象。这不符合设计原则中的“单一职责原则”,这是一种很差的作法。假设咱们某天须要利用这个类,在页面中建立不少个div,即让这个类从单例类编程一个普通的能够产生多个实例的类,咱们就得改写createDiv构造函数,把控制建立惟一对象的那一段去掉,这种修改会给咱们带来没必要要的烦恼。浏览器
如今咱们经过引入代理类的方法,来解决上面提到的问题。缓存
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); }; // 引入代理类 proxySingletonCreateDiv var proxySingletonCreateDiv = (function(){ var instance; return function(html){ if(!instance){ instance = new createDiv(html); } return instance; } })(); var a = new proxySingletonCreateDiv('sin1'); var b = new proxySingletonCreateDiv('sin2');
咱们把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv中。这样一来,createDiv就变成了一个普通的类,它跟proxySingletonCreateDiv组合起来就能够达到单例模式的效果;若是单独使用,就做为一个普通的类,能产生多个实例对象。闭包
前面提到的单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从类中建立而来。在以类为中心的语言中,这是很天然的作法,好比在Java中,若是须要某个对象,就必须先定义一个类,对象老是从类中建立而来。
但JavaScript是一门无类语言,生搬单例模式的概念并没有意义。在JavaScript中建立对象很是简单,直接声明便可。既然这样,咱们就没有必要为它先建立一个类。
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但在JavaScript开发中,咱们常常会把全局变量当成单例模式来使用,例如var a = {};
。
当用这种方式建立对象a时,对象a确实独一无二。若是变量a被声明在全局做用域下,则咱们能够在代码中的任何位置使用这个变量,全局变量天然能全局访问。这样就知足了单例模式的两个条件。
可是,全局变量存在一些问题:
容易形成命名空间污染;
在大型项目中,若是不加以限制和管理,程序中可能存在不少这样的变量;
JavaScript中的变量很容易被不当心覆盖。
所以,在使用全局变量时,咱们要尽力下降它的污染,经过如下方式:
1.使用命名空间
适当地使用命名空间,并不会杜绝全局变量,但能够减小全局变量的数量。
最简单的方法依然是用对象字面量的方式:
var namespace1 = { a: function(){ alert(1); }, b: function(){ alert(2); } };
把a和b都定义为namespace1的属性,这样能够减小变量和全局做用域打交道的机会。‘
另外,能够动态地建立命名空间,如:
var myApp = {}; myApp.namespace = function(name){ var parts = name.split('.'); var current = myApp; for(var i in parts){ if(!current[parts[i]]){ current[parts[i]] = {}; } current = current[parts[i]]; } }; myApp.namespace('event'); myApp.namespace('dom.style');
上述代码等价于:
var myApp = { event:{}, dom:{ style:{} } };
2.使用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通讯:
var user = (function(){ var __name = 'sin1'; var __age = 29; return { getUserInfo: function(){ return __name + '-' + __age; } } })();
咱们用下划线来约定私有变量__name和__age,它们被封装在闭包产生的做用域中,外部是访问不到这两个变量的,这就避免了对全局的命名污染。
惰性单例指的是在须要的时候才建立对象实例。惰性单例在实际开发中很是有用,是单例模式的重点。
咱们在开头写的Singleton类就用过这种技术,instance实例对象老是在咱们调用Singleton.getInstance的时候才被建立,而不是在页面加载好的时候就建立。
假设,在一个提供登陆功能(点击登陆按钮弹出一个登陆悬浮窗)的web页面中,可能用户在访问过程当中,根本不须要进行登陆操做,只须要浏览某些内容。因此,没有必要在页面加载好以后就立刻建立登陆悬浮窗,只须要当用户点击登陆按钮的时候才开始建立登陆悬浮窗,实现代码以下:
<!DOCTYPE html> <html> <head> <title>惰性单例</title> </head> <body> <button id = "loginBtn">登陆</button> </body> <script type="text/javascript"> 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'; }; </script> </html>
但这段代码仍是存在一些问题的:
这段代码仍然是违反单一职责原则的,建立对象和管理单例的逻辑都放在createLoginLayer对象内部;
若是咱们下次须要建立页面中惟一的iframe,或者script标签,必须得如法炮制,把createLoginLayer函数几乎照抄一遍。
为了解决上面的问题,咱们能够实现一段通用的惰性单例代码:
<!DOCTYPE html> <html> <head> <title>惰性单例</title> </head> <body> <button id = "loginBtn">登陆</button> </body> <script type="text/javascript"> var getSingle = function(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'; }; // 当须要建立惟一的iframe用于加载第三方页面时 var createSingleIframe = getSingle(function(){ var iframe = document.createElement('iframe'); document.body.appendChild(iframe); return iframe; }); document.getElementById('loginBtn').onclick = function(){ var loginLayer = createSingleIframe(); loginLayer.src = 'http://baidu.com'; }; </script> </html>
上面的代码,
把管理单例的逻辑抽象了出来:用一个变量来标志是否建立过对象,若是是,则在下次直接返回这个已经建立好的对象;
把如何管理单例的逻辑封装在getSingle函数内部,建立对象的方法fn被当成参数动态传入getSingle函数;
将建立登陆悬浮窗的方法传入getSingle,还能传入createIframe,createScript;
getSingle函数返回一个新的函数,而且用一个变量result来保存fn的计算结果,result变量在闭包中,永远不会被销毁,因此在未来的请求中,若是result已经被赋值,那么它将返回这个值。
单例模式的用途不止在于建立对象,好比咱们一般渲染完页面中的一个列表后,就要给这个列表绑定click事件,若是经过ajax动态往列表里追加数据,在使用事件代理的前提下,click事件实际上只须要在第一次渲染列表的时候就被绑定一次。
<!DOCTYPE html> <html> <head> <title>惰性单例</title> </head> <body> <button id = "renderBtn">渲染列表</button> </body> <script type="text/javascript"> var getSingle = function(fn){ var result; return function(){ return result || (result = fn.apply(this, arguments)); } }; var bindEvent = getSingle(function(){ console.log('绑定click事件'); document.getElementById('renderBtn').onclick = function(){ alert('click'); } return true; }); var render = function(){ console.log('开始渲染'); bindEvent(); } render(); render(); render(); // 最终输出结果: // 开始渲染 // 绑定click事件 // 开始渲染 // 开始渲染 </script> </html>
PS:本节内容为《JavaScript设计模式与开发实践》第四章 笔记。