设计模式|JavaScript实现单例模式(上篇)

5,511 阅读4分钟

这是我参与更文挑战的第21天,活动详情查看:更文挑战

什么是单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就是单例模式,也是单例模式的定义。

单例模式的用途非常广,像实现全局模态框,像在Redux和Vuex中的Store实现,就是单例模式的典型应用。

实现简单的单例模式

实现一个简单的单例模式并不复杂,核心知识点在于使用一个变量来标志这个类是否已经创建过对象实例,如果是,则直接返回之前创建的对象实例,如果为否,则创建这个类的对象实例,并保存到这个变量标志中。

代码实现

ES5实现

完整代码

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;
};

测试用例

let a = Singleton.getInstance( 'sven1' );
let b = Singleton.getInstance( 'sven2' );
console.log ( a === b ); // true

ES6实现

完整代码

/**
 * 单例模式
 */
class Singleton {
  constructor(name) {
    this.name = name;
    this.instance = null; // 单例模式的标识 
  }

  getName() {
    console.log(this.name);
  }
  /**
   * 获取单例的静态方法
   */
  static getInstance(name) {
    // 如果已经创建过对象实例,则直接返回之前创建的对象实例,如果没有,则创建这个类的对象实例,病保存到instance这个单例变量标识中
    if (!this.instance) {
      this.instance = new Singleton(name);
    }
    return this.instance;
  }
}

测试用例

let a = Singleton.getInstance( 'sven1' );
let b = Singleton.getInstance( 'sven2' );
console.log ( a === b ); // true

透明的单例模式

上面的例子中,我们实现了简单的单例模式,虽然这种方法使用起来很简单,但是对使用者而言,需要提前知道这个类是一个"单例类",才知道该使用getInstance来获取对象实例。

为了实现单例模式的"透明"特性,在ES5中,我们要用上自执行匿名函数和闭包,利用闭包存储公共的单例类实例,使用自执行匿名函数返回真正的单例类的构造方法;而在ES6中,我们可以利用类的构造器返回公共的单例类实例。

代码实现

ES5实现

完整代码

/**
 * 透明单例模式
 * @param {*} html 
 */
 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( 'sven1' );
var b = new CreateDiv( 'sven2' );

console.log(a === b);
// true

ES6实现

完整代码

/**
 * 透明单例模式
 */
class CreateDiv {
  constructor(html) {
    if (!CreateDiv.instance) {
      CreateDiv.instance = this;
      this.html = html;
      this.init();
    }
    return CreateDiv.instance;
  }

  init () {
    var div = document.createElement( 'div' );
    div.innerHTML = this.html;
    document.body.appendChild( div );
  };
}

测试用例

let a = new CreateDiv( 'sven1' );
let b = new CreateDiv( 'sven2' );

console.log(a === b);
// true

用代理实现单例模式

虽然上述这种透明的单例类写法可以让使用者直接使用new关键字的获取单例类实例,但是一定程度增加了程序阅读的复杂度,还有可能破坏了"单一职责原则"。如果以后需要为其他类实现透明的单例,就要重复为这个类重新实现一遍透明单例类,这怎么看都不是一种很好的解决方法。

利用ES6的代理(proxy),来解决这个问题,就可以让单例实现和类本身进行解耦。在ES5中,由于并不支持代理(proxy)的特性,我们需要自己实现一个代理类。

代码实现

ES5实现

完整代码

var CreateElement = function( html ){
  this.html = html;
  this.init();
};
CreateElement.prototype.init = function(){
 var div = document.createElement( 'div' );
 div.innerHTML = this.html;
 document.body.appendChild( div );
};
var ProxySingletonCreateElement = (function(CreateElement){
 var instance;
 return function(){
   if (!instance) {
     // 为了便捷,这里使用ES6才支持的扩展运算符,也可以使用类似eval的方式模拟
     instance = new CreateElement(...arguments)
 }
 return instance;
 }
})(CreateElement);

测试用例

var a = new ProxySingletonCreateElement( 'sven1' );
var b = new ProxySingletonCreateElement( 'sven2' );
console.log ( a === b );
// true

ES6实现

完整代码

/**
 * 透明单例模式
 */
class CreateDiv {
  constructor(html) {
    this.html = html;
    this.init();
  }
  init() {
    let div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }
}

function singletonProxy(className) {
  let instance = null;
  return new Proxy(className, {
    /**
      * @description construct()方法用于拦截new命令,可以接受三个参数
      * @param target 目标对象
      * @param args 构造函数的参数数组
      * @param newTarget 创造实例对象时,new命令作用的构造函数
      */
    construct(target, args) {
      // 内部类
      class ProxyClass {
        constructor() {
          if (instance === null) {
            instance = new target(...args);
          }
          return instance;
        }
      }
      return new ProxyClass();
    },
  });
}

测试用例

let createSothx = singletonProxy(CreateDiv)
let sothx1 = createSothx('sothx1')
let sothx2 = createSothx('sothx2')
console.log(sothx1 === sothx2)
// true

参考资料

《JavaScript设计模式与开发实践》——单例模式

www.ituring.com.cn/book/1632

Proxy代理实现单例模式

juejin.cn/post/684490…