单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

实现一个全局唯一的Modal浮窗

假设我们是WebQQ的开发人员,当点击左边导航里QQ头像时,会弹出一个登录浮窗(如图所示),很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。

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

传统面向对象单例模式

要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。代码如下:

  1. class SingletonLoginLayer {
  2. static getInstance() {
  3. // 判断是否已经new过1个实例
  4. if (!SingletonLoginLayer.instance) {
  5. // 若这个唯一的实例不存在,那么先创建它
  6. SingletonLoginLayer.instance = new SingletonLoginLayer()
  7. }
  8. // 如果这个唯一的实例已经存在,则直接返回
  9. return SingletonLoginLayer.instance
  10. }
  11. constructor() {
  12. this.div = document.createElement( 'div' );
  13. this.div.innerHTML = '我是登录浮窗';
  14. document.body.appendChild( this.div );
  15. }
  16. }
  17. document.getElementById( 'loginBtn' ).onclick = function(){
  18. var loginLayer = SingletonLoginLayer.getInstance().div;
  19. };

我们通过SingletonLoginLayer.getInstance来获取SingletonLoginLayer类的唯一对象,这种方式相对简单,但有一个问题,就是增加了这个类的“不透明性”, SingletonLoginLayer类的使用者必须知道这是一个单例类。

我们现在的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。首先在CreateLoginLayer构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类:

  1. class CreateLoginLayer {
  2. constructor() {
  3. this.div = document.createElement( 'div' );
  4. this.div.innerHTML = '我是登录浮窗';
  5. document.body.appendChild( this.div );
  6. }
  7. }

接下来引入代理类proxySingletonCreateLoginLayer:

  1. const proxySingletonCreateLoginLayer = (function() {
  2. let instance;
  3. return function() {
  4. if (!instance) {
  5. instance = new CreateLoginLayer();
  6. }
  7. return instance;
  8. }
  9. })()
  10. document.getElementById( 'loginBtn' ).onclick = function(){
  11. let createLoginLayer = new proxySingletonCreateLoginLayer();
  12. let loginLayer = createLoginLayer.div;
  13. };

通过引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类proxySingletonCreateLoginLayer中。这样一来,CreateLoginLayer就变成了一个普通的类,它跟proxySingletonCreateLoginLayer组合起来可以达到单例模式的效果。

javascript中的单例模式

我们可以用一个变量来判断是否已经创建过登录浮窗

  1. const createLoginLayer = (function(){
  2. let div;
  3. return function(){
  4. if ( !div ){
  5. div = document.createElement( 'div' );
  6. div.innerHTML = ’我是登录浮窗’;
  7. document.body.appendChild( div );
  8. }
  9. return div;
  10. }
  11. })();
  12. document.getElementById( 'loginBtn' ).onclick = function(){
  13. let loginLayer = createLoginLayer();
  14. };

实际上,上面设计的单例模式并不优雅,还存在一些问题。

  • 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内部。
  • 如果我们下次需要创建页面中唯一的iframe,或者script标签,用来跨域请求数据,就必须得如法炮制,把createLoginLayer函数几乎照抄一遍

我们需要把不变的部分隔离出来,先不考虑创建一个div和创建一个iframe有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的。

现在我们就把如何管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数:

  1. const getSingle = function( fn ){
  2. let result;
  3. return function(){
  4. return result || ( result = fn.apply(this, arguments ) );
  5. }
  6. };

接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr等。之后再让getSingle返回一个新的函数,并且用一个变量result来保存fn的计算结果。result变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果result已经被赋值,那么它将返回这个值。代码如下:

  1. const createLoginLayer = function(){
  2. let div = document.createElement( 'div' );
  3. div.innerHTML = ’我是登录浮窗’;
  4. document.body.appendChild( div );
  5. return div;
  6. };
  7. let createSingleLoginLayer = getSingle( createLoginLayer );
  8. document.getElementById( 'loginBtn' ).onclick = function(){
  9. let loginLayer = createSingleLoginLayer();
  10. };

总结

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