hawtim.github.io icon indicating copy to clipboard operation
hawtim.github.io copied to clipboard

创建型模式-单例模式

Open hawtim opened this issue 5 years ago • 0 comments

定义

单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

场景

有一些对象我们往往只需要一个,比如 线程池,全局缓存,浏览器中的 window 对象。

在 javascript 中的单例模式

单例模式更加适合传统的面向对象语言中的类。在 javascript 中生搬单例模式的概念并无意义。

js 中两种单例模式的体现,本质都是减少全局变量,因为全局变量也可以理解为一种单例

  1. 使用命名空间
  2. 使用闭包封装私有变量

不透明的/标准的单例模式

class Singleton {
  // 静态私有成员变量
  // 提供全局访问的方法
  static getInstance(name) {
    // 确保一个类只有一个实例
    if (!this.instance) {
      // 自行实例化并提供这个实例
      this.instance = new Singleton(name)
    }
    return this.instance
  }
  // 私有构造函数
  constructor(name) {
    this.name = name
    this.instance = null
  }
  // 公有的静态工厂方法
  getName() {
    return this.name
  }
}
// 简单的测试
let sven1 = Singleton.getInstance('sven1')
let sven2 = Singleton.getInstance('sven2')

console.log(sven1 === sven2) // true

上述的例子中,不透明的地方体现在我们不知道这是一个单例类,并且跟以往通过 new XXX 的方式来获取对象不同,这里偏要使用 Singleton.getInstance 来获取对象。

透明的单例模式

要实现一个透明的单例模式就需要允许我们使用 new 操作符来获取对象。 我们来看一个在页面中创建唯一的 div 节点的例子。

let CreateDiv = (function() {
  let instance
  let CreateDiv = function(html) {
    if (instance) {
      return instance
    }
    this.html = html
    this.init()
    return instance = this
  }
  CreateDiv.prototype.init = function() {
    let div = document.createElement('div')
    div.innerHTML = this.html
    document.body.appendChild(div)
  }
  // 使用闭包返回真正的 Singleton 构造方法
  return CreateDiv
})()

let a = new CreateDiv('sven1')
let b = new CreateDiv('sven2')
console.log(a === b)

上述的代码使用自执行的匿名函数和闭包,并且让这个匿名函数返回真正 Singleton 构造方法,增加了一些程序的复杂度,阅读起来也不是很舒服。

使用代理实现单例模式

把负责管理单例的逻辑移到了代理类 proxySingletonCreateDiv

// 改造成创建 div 的类,将控制唯一实例的逻辑在代理中实现
class CreateDiv {
  constructor(html) {
    this.html = html
    this.init()
  }
  init() {
    let div = document.createElement('div')
    div.innerHTML = this.html
    document.body.appendChild(div)
  }
}
// 使用代理类实现 proxySingletonCreateDiv
let ProxySingletonCreateDiv = (function(){
  let instance
  return function(html) {
    if (!instance) {
      instance = new CreateDiv(html)
    }
    return instance
  }
})()

let a = new ProxySingletonCreateDiv('sven1')
let b = new ProxySingletonCreateDiv('sven2')

console.log(a === b)

构造一个通用的代理函数

class CreateDiv {
  constructor(html) {
    this.html = html
    this.init()
  }
  init() {
    let div = document.createElement('div')
    div.innerHTML = this.html
    document.body.appendChild(div)
  }
}
// 通用的单例代理函数
function commonProxySingleton(funClass) {
  let instance
  let funClass = funClass
  return function() {
    if (!instance) {
      instance = new funClass(arguments)
    }
    return instance
  }
}
// 因为只是一个普通函数,所以不使用 new
let proxySingletonCreateDiv = commonProxySingleton(CreateDiv)
let a = proxySingletonCreateDiv('sven1')
let b = proxySingletonCreateDiv('sven2')
// 使用 new 的话 this 指向为 commonProxySingleton 的实例
// let ProxySingletonCreateDiv = new commonProxySingleton(CreateDiv)
// let c = ProxySingletonCreateDiv('sven3')
console.log(a === b)

惰性单例

在需要实例化对象的时候才创建实例,将创建实例对象的职责和管理单例的职责分别开来,独立不互相影响。

利用闭包来实现。

// 惰性单例
let createLoginLayer = (function() {
  let div
  return function() {
    if (!div) {
      div = document.createElement('div')
      div.innerHTML = '我是登录浮窗'
      div.style.display = 'none'
      document.body.appendChild(div)
    }
    return div
  }
})()

document.getElementById('loginBtn').onclick = function() {
  let loginLayer = createLoginLayer()
  loginLayer.style.display = 'block'
}

通用的惰性单例

let getSingle = function(fn) {
  let result
  return function() {
    return result || (result = fn.apply(this, arguments))
  }
}
// 创建登录浮窗
let createLoginLayer = function() {
  let div = document.createElement('div')
  div.innerHTML = '我是登录浮窗'
  div.style.display = 'none'
  document.body.appendChild(div)
  return div
}
// 惰性函数包裹
let createSingleLoginLayer = getSingle(createLoginLayer)
document.getElementById('loginBtn').onclick = function() {
  let loginLayer = createSingleLoginLayer()
  loginLayer.style.display = 'block'
}
// 直接传入回调函数创建 iframe
let createSingleIFrame = getSingle(function() {
  let iframe = document.createElement('iframe')
  document.body.appendChild(iframe)
  return iframe
})
document.getElementById('loginBtn').onclick = function() {
  let loginLayer = createSingleIFrame()
  loginLayer.src = 'http://baidu.com';
}

优点

  • 提供了对唯一实例的受控访问。可以严格控制客户怎样以及何时访问它。
  • 减少资源的消耗。由于在系统内存中只存在一个对象,因此可以节约系统资源。对于需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”,因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

使用场景

  • 只需要一个实例对象,如要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  • 要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式

总结

  • 单例模式是一种创建型模式。
  • 单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
  • 单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法。该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。

hawtim avatar Oct 10 '20 05:10 hawtim