单例设计模式(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函数中,实际上也提到了闭包和高阶函数的概念。单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。
