单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
实现一个全局唯一的Modal浮窗
假设我们是WebQQ的开发人员,当点击左边导航里QQ头像时,会弹出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。
传统面向对象单例模式
要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。代码如下:
class SingletonLoginLayer {
static getInstance() {
// 判断是否已经new过1个实例
if (!SingletonLoginLayer.instance) {
// 若这个唯一的实例不存在,那么先创建它
SingletonLoginLayer.instance = new SingletonLoginLayer()
}
// 如果这个唯一的实例已经存在,则直接返回
return SingletonLoginLayer.instance
}
constructor() {
this.div = document.createElement( 'div' );
this.div.innerHTML = '我是登录浮窗';
document.body.appendChild( this.div );
}
}
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = SingletonLoginLayer.getInstance().div;
};
我们通过SingletonLoginLayer.getInstance来获取SingletonLoginLayer类的唯一对象,这种方式相对简单,但有一个问题,就是增加了这个类的“不透明性”, SingletonLoginLayer类的使用者必须知道这是一个单例类。
我们现在的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。首先在CreateLoginLayer构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类:
class CreateLoginLayer {
constructor() {
this.div = document.createElement( 'div' );
this.div.innerHTML = '我是登录浮窗';
document.body.appendChild( this.div );
}
}
接下来引入代理类proxySingletonCreateLoginLayer:
const proxySingletonCreateLoginLayer = (function() {
let instance;
return function() {
if (!instance) {
instance = new CreateLoginLayer();
}
return instance;
}
})()
document.getElementById( 'loginBtn' ).onclick = function(){
let createLoginLayer = new proxySingletonCreateLoginLayer();
let loginLayer = createLoginLayer.div;
};
通过引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类proxySingletonCreateLoginLayer中。这样一来,CreateLoginLayer就变成了一个普通的类,它跟proxySingletonCreateLoginLayer组合起来可以达到单例模式的效果。
javascript中的单例模式
我们可以用一个变量来判断是否已经创建过登录浮窗
const createLoginLayer = (function(){
let div;
return function(){
if ( !div ){
div = document.createElement( 'div' );
div.innerHTML = ’我是登录浮窗’;
document.body.appendChild( div );
}
return div;
}
})();
document.getElementById( 'loginBtn' ).onclick = function(){
let loginLayer = createLoginLayer();
};
实际上,上面设计的单例模式并不优雅,还存在一些问题。
- 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内部。
- 如果我们下次需要创建页面中唯一的iframe,或者script标签,用来跨域请求数据,就必须得如法炮制,把createLoginLayer函数几乎照抄一遍
我们需要把不变的部分隔离出来,先不考虑创建一个div和创建一个iframe有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的。
现在我们就把如何管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数:
const getSingle = function( fn ){
let result;
return function(){
return result || ( result = fn.apply(this, arguments ) );
}
};
接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr等。之后再让getSingle返回一个新的函数,并且用一个变量result来保存fn的计算结果。result变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果result已经被赋值,那么它将返回这个值。代码如下:
const createLoginLayer = function(){
let div = document.createElement( 'div' );
div.innerHTML = ’我是登录浮窗’;
document.body.appendChild( div );
return div;
};
let createSingleLoginLayer = getSingle( createLoginLayer );
document.getElementById( 'loginBtn' ).onclick = function(){
let loginLayer = createSingleLoginLayer();
};
总结
在getSinge函数中,实际上也提到了闭包和高阶函数的概念。单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。