单例模式

138 阅读4分钟

什么是单例模式

一个构造函数只能有一个实例对象,提供全局访问的功能,如果创建多次,会得到与第一次创建对象完全相同的对象

单例模式的应用

一个实际场景,当我们点击登陆按钮时,页面中可能会出现一个弹框,而这个弹框是唯一的,无论点多少次登陆按钮,弹框只会被创建一次,那么这种情况下就适合用单例模式来创建弹框。

单例的实现方式

1、简单单例模式

通过自执行函数和闭包将instance封装起来。并且返回了真正的Singleton构造方法

var CreateLogin = (function () {
  var instance = null;
  var CreateLogin = function (html) {
    this.html = html;
    if (!instance) {
      instance = this.init();
    }
    return instance;
  }
  CreateLogin.prototype.init = function () {
    var div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild(div);
    return this;
  }
  return CreateLogin;
})();

上诉方法实现用了两步

  • 创建对象,init初始化登录弹窗
  • 只返回一个实例对象

存在的问题

  • 违背单一职责原则(如果html结构变更)
  • init方法在实例中可以调用,在body里添加相应的dom结构

2、代理单例模式

代理模式,自己不做,委托别人做

  • 将 createLogin 改写成只负责创建login弹窗的类
  • 引入代理类
var CreateLogin = function (html) {
  this.html = html;
  this.init();
}
CreateLogin.prototype.init = function () {
  var div = document.createElement('div');
  div.innerHTML = this.html;
  document.body.appendChild(div);
}
var proxySingleton = (function () {
  var instance = null;
  return function (html) {
    return instance || (instance = new CreateLogin(html))
  }
})();
var login1 = proxySingleton('login1');
var login2 = proxySingleton('login2');
login1 === login2; // true
login1.init() // 重新创建一个div并加入到body中

还存在init方法的暴露,我们可以对CreateLogin 做进一步的封装

var CreateLogin = (function() {
  function CreateLogin(html) {
    this.html = html;
    _init.call(this);
  }
  var _init = function () {
    var div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }
  return CreateLogin
})();
var proxySingleton = (function () {
  var instance = null;
  return function (html) {
    return instance || (instance = new CreateLogin(html))
  }
})();
var login1 = proxySingleton('login1');
login.init() // 报错
// 扩展proxySingleton,支持其他类,比如注册,类似工厂函数
var proxySingleton = (function () {
  var instance = null;
  return function () {
    var construction = Array.prototype.shift.call(arguments);
    var args = Array.prototype.slice.call(arguments);
    return instance || (instance = new construction(args))
  }
})();
var login1 = proxySingleton(CreateLogin, 'login1');

3、惰性单例模式

惰性单例是指需要的时候才去创建实例,不像简单单例,利用自执行函数加载的时候就创建实例

比如,当打开一个网站时,需要登录,但登陆的弹窗只会在点击登陆按钮时出现,甚至有的网站不需要登录就能直接浏览。这时我们并不需要在页面加载时就去创建一个弹窗。我们大可在需要用的时候去创建。

var createLazyDiv = (function () {
  var div = null;
  return function () {
    if (!div) {
      div = document.createElement('div');
      div.innerHTML = 'create lazy div';
      document.body.appendChild(div);
    }
    return div;
  }
})();
document.getElementById('loginBtn').onclick = function() {
    var loginLazy = createLazyDiv();
}

以上我们实现了一个惰性单例的登录。但是我们还是可以把其中的控制只有一个对象的操作抽离出来,让我们来实现一个通用的惰性单例

// 把创建div的方法抽离出来
var createLazyDiv = function (html) {
  var div = document.createElement('div');
  div.innerHTML = html;
  document.body.appendChild(div);
  return div;
}
// 把只创建一个实例的方法抽离出来,供其他需要的使用
var getSingleton = (function () {
  var instance = null;
  return function() {
    var fn = Array.prototype.shift.call(arguments);
    var args = Array.prototype.slice.call(arguments);
    return instance || (instance = fn.apply(this, args));
  }
})();
document.getElementById('loginBtn').onclick = function() {
    var loginLazy = getSingleton(createLazyDiv, 'login1');
}

到此,我们实现了一个 getSingleton 函数来帮我们实现只有一个实例对象的目的,并且将实例对象要做的指责独立出来,两个方法互不打扰,并可以提供其他需要实现使用惰性单例的需求,方法

ES6实现

上述是ES5的实现方式,现在ES6已经广泛使用,所以实现一套ES6的单例

class Login {
  constructor (html) {
    this.html = html;
  }
  static getInstance (html) {
    if (!this.instance) {
      this.instance = new Login(html)
    }
    return this.instance
  }
}
// 效果
const login1 = Login.getInstance('i am div1')
const login2 = Login.getInstance('i am div2')
console.log(login1 === login2) // true

// 单一职责原则
// 抽离出一份只负责实例对象的方法,实现单例
class Singleton {
  constructor () {
    if (new.target === Singleton) {
      throw new Error('本类不能实例化')
    }
  }
  static getSingleton (html) {
    if (!this.instance) {
      this.instance = new this(html)
    }
    return this.instance
  }
}
// 继承自单例方法
class Login extends Singleton {
  constructor(html) {
    super()
    this.html = html
  }
}
const login1 = Login.getSingleton('i am login1');
const login2 = login.getSingleton('i am login2');
console.log(login1 === login2) // true

后续

在简单模式里,有使用到new 的方式实例化一个对象,那么如果没有使用new 会发生什么? 提到了工厂模式 下一节会介绍下工厂模式,以及其应用