一、概念
单例模式,我的理解是,单例单例,只有一个实例。如果一个类,不管它实例化多少次,只能实例化出来一个实例,并且是第一实例化的那个实例,那么这种情况就叫单例模式
二、单例模式的作用
单例模式有什么好处,为什么我们要使用单例模式?
单例有点像全局对象的作用,但是全局变量可能不小心被覆盖掉,那不玩完了
所以单例模式第一个好处就是具有类似全局对象的唯一性,同时又避免像全局变量那样不好管控
此外,你要知道new
本身是有内存开销的,单例模式由于始终只能得到一个实例,从而避免了频繁低创建和销毁实例,减少内存占用
三、单例模式的实现
“简单版”单例模式
一般地,如果我们多次去new一个类,会得到多个实例,就像这样
我可以定义一个getInstance
方法,使用Person去调这个方法,不管我调多少次,返回的永远都是第一次调的实例。
getInstance里面最关键
的就是使用了if判断,当实例存在时就不再去new了,而是直接返回之前new得到的实例
const Person = function (name, age) {
this.name = name
this.age = age
this.instance = null
}
Person.getInstance = function (name, age) {
if (this.instance) {
return this.instance
}
return this.instance = new Person(name, age)
}
const tianchou = Person.getInstance('尹天仇', 20)
const piaopiao = Person.getInstance('柳飘飘', 19)
console.log(tianchou)
console.log(piaopiao)
console.log(tianchou === piaopiao)
得到的结果:
但这种实现方式不太符合我们日常实例化对象的写法,通常我们是使用new关键字来实例化的,这里通过调Person的getInstance方法来实例化
“透明版”单例模式
“透明版”单例模式就是用来解决“简单版”单例模式不能使用new来实例化对象的问题,但其实它们的核心思想都是一样的,都是在实例化时,先判断实例存不存在,如果存在直接返回,不存在时才会去创建实例
const Person = (function () {
let instance = null
return function (name, age) {
this.name = name
this.age = age
if (!instance) {
instance = this
}
return instance
}
})()
const tianchou = new Person('尹天仇', 20)
const piaopiao = new Person('柳飘飘', 19)
console.log(tianchou)
console.log(piaopiao)
console.log(tianchou === piaopiao)
“代理版”单例模式
“透明版”单例模式存在一个问题:创建对象的操作和管理单例的操作分开
“代理版”单例模式就是将创建对象的操作与管理单例的操作拆分开,实现一个函数就干一件事
const Person = function (name, age) {
this.name = name
this.age = age
}
// 实现单例模式
const PersonProxy = (function () {
let instance = null
return function (name, age) {
if (!instance) {
instance = new Person(name, age)
}
return instance
}
})()
const tianchou = new PersonProxy('尹天仇', 20)
const piaopiao = new PersonProxy('柳飘飘', 19)
console.log(tianchou)
console.log(piaopiao)
console.log(tianchou === piaopiao)
通过PersonProxy方法,实现了代理的逻辑,不用动原来Person的逻辑,满足设计模式的开闭原则(对扩展关闭,对修改开放)
但是这种方式,如果我还要将一个Dog类也要弄个DogProxy类出来,那实现单例模式的逻辑不得又写一遍?
所以咱们可以定义一个singleton方法,专门用来将普通类转换为一个可以实现单例的类
// 将普通类转换为一个可以实现单例的类
const singleton = function (className) {
let instance = null
return function () {
if (!instance) {
instance = new className(...arguments)
}
return instance
}
}
如此,PersonProxy就可以用singleton得到:
const PersonProxy = singleton(Person)
敲黑板!!! 这样还是有问题呀,如果想要给PersonProxy类添加原型方法,通过实例tianchou调用,能调用吗?
PersonProxy.prototype.sayHello = () => {
console.log('我养你啊~')
}
tianchou.sayHello()
调用个锤锤,为什么呢?
在new PersonProxy
时,实际上是在对singleton函数中返回的那个匿名函数进行实例化,咱们在这个函数中打印this看看,sayHello搁这呢:
需要对singleton方法进行改造,使用proxy代理:
const singleton = function (className) {
let instance = null
return new Proxy(className, {
construct(target, args) {
if (!instance) {
instance = new target(...args)
}
return instance
}
})
// return function () {
// if (!instance) {
// instance = new className(...arguments)
// }
// return instance
// }
}
如此,tianchou终于说出了那句“我养你啊~”
惰性单例模式(代理模式和懒加载的结合)
所谓惰性单例,和懒加载是一个道理,就是当我需要实例时再进行实例化。惰性单例的一个典型例子就是,点击按钮,创建一个提示,那一定是点击的时候才进行创建,你不能初始化就创建好了,那就不是按需加载了;另外要满足“单例”,就意味着多次点击,永远都是生成一个实例
依然是使用singleton函数得到一个代理类
// 创建div
const CreateDiv = function (html) {
const div = document.createElement('div')
div.innerHTML = html
div.style.display = 'none'
document.body.appendChild(div)
return div
}
// 使用singleton得到一个可以用来实例化并且永远只有一个实例的类
const DivProxy = singleton(CreateDiv)
const btn = document.getElementById('btn')
btn.addEventListener('click', () => {
const divInstance = new DivProxy('提示')
divInstance.style.display = 'block'
})
效果就是,无论点击多少次,都只会创建一个div元素
原本我们可以在页面中写好一个div,通过display属性控制显示隐藏,这样也是全局只有一个提示div,但问题是不管用户有没有看这个弹框的需求,它都会创建好并添加到body中,那这个时候就可以考虑使用单例模式,来避免这种性能上的浪费
四、单例模式的应用场景
单例模式的特点是唯一性和全局性,由此我们可以想到Window、document、全局存储、全局loading等,这些全局的属性或方法,一定是唯一的,否则会带来严重的性能开销和访问行为不一致
浏览器中的window和document全局对象
这2个对象在任何时候访问都是一样的对象
ES6的import、export导入导出,模块中的变量是单例的
在某个地方改变了模块内部变量的值,其他地方再次引用是改变之后的值
模态框、弹框、message等
就是上面惰性单例那个例子
全局状态管理
vuex、router等这些变量也是全局性的,当在一个地方改变后,其他地方跟着变化