Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

创建型模式-单例模式 #44

Open
hawtim opened this issue Oct 10, 2020 · 0 comments
Open

创建型模式-单例模式 #44

hawtim opened this issue Oct 10, 2020 · 0 comments

Comments

@hawtim
Copy link
Owner

hawtim commented Oct 10, 2020

定义

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

场景

有一些对象我们往往只需要一个,比如 线程池,全局缓存,浏览器中的 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关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法。该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant