单例模式:实现一个全局唯一的Modal浮窗

2,146 阅读4分钟

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