hawtim.github.io
hawtim.github.io copied to clipboard
创建型模式-单例模式
定义
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
场景
有一些对象我们往往只需要一个,比如 线程池,全局缓存,浏览器中的 window 对象。
在 javascript 中的单例模式
单例模式更加适合传统的面向对象语言中的类。在 javascript 中生搬单例模式的概念并无意义。
js 中两种单例模式的体现,本质都是减少全局变量,因为全局变量也可以理解为一种单例
- 使用命名空间
- 使用闭包封装私有变量
不透明的/标准的单例模式
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关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法。该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。