定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的设计模式,有一些对象往往在全局中有且只需要一个,比如线程池,全局缓存,浏览器对象等,在JavaScript开发中使用也十分广泛,比如登录浮窗,不管我们点击多少次,这个浮窗应该只会创建一次,之后的创建应该是直接返回已创建好的浮窗模块,那么整个创建浮窗的逻辑就适合用单例模式来实现
下面我们先来用JavaScript实现一下类语言的简单单例模式
let singLeton = function (name) {
this.name = name
}
singLeton.instance = null
singLeton.prototype.getName = function () {
console.log(this.name)
}
singLeton.getInstance = function (name) {
if (!this.instance) {
this.instance = new singLeton(name)
}
return this.instance
}
let a = singLeton.getInstance('tom')
let b = singLeton.getInstance('jerry')
console.log(a, b, a === b)
输出结果:
以上就是一个简单的单例模式的实现,但是这种单例模式意义并不大,正常创建一个单例应该是通过 new的方式创建,而不是需要使用者知道某个属性是单例模式的接口,下面我们再实现一种更为通用的单例模式
透明单例模式
let createDiv = (function (html) {
let instance = null
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)
}
return CreateDiv
})()
let a = new createDiv('tom')
let b = new createDiv('jerry')
console.log(a, b, a === b)
仔细观察这段代码,在内层的CreateDiv中其实做了很多事,1.创建对象和初始化init方法,2.保证只有一个实例对象。实际上这是违反“单一职责原则”的,也就是一个方法里只能执行一种功能,显然这样是不好的,假设我们每天要利用这个类,在页面中创建成千上百的div,这个时候我们就必须的改写CreateDiv构造函数,使其变成可以创建多个实例的类,而且还要把控制创建唯一对象的那段代码去掉,这样修改起来十分苦恼,我们可以使用代理的方式,抽离出控制创建唯一实例对象的那段代码,然后将CreateDiv改造成普通的创建div的类,得到如下代码
代理实现单例模式
let CreateDiv = function (html) {
this.html = html
this.init()
}
CreateDiv.prototype.init = function () {
let div = document.createElement("div")
div.innerHTML = this.html
document.body.appendChild(div)
}
// 引入代理类
const ProxySingletonCreateDiv = (function () {
let instance = null
return function (html) {
if (instance) return instance
return instance = new CreateDiv(html)
}
})()
let a = new ProxySingletonCreateDiv('tom')
let b = new ProxySingletonCreateDiv('jerry')
console.log(a, b, a === b)
不难看出,这样实现之后,不仅可以通过普通方式创建多个div,也可以通过代理类实现单例模式
以上就是类语言中实现方式,但是JavaScript严格意义上讲是一个无类的语言,在JavaScript中创建对象是十分简单的,既然我们只需要一个"唯一"的对象,为什么我们还要去创建一个创建对象的类呢? 这不就是脱裤子放屁多此一举嘛,所以以上的传统方式在JavaScript中不太适用。
同时,由于JavaScript中存在全局变量,所以在JavaScript中经常将全局变量当做单例来使用,虽然这种创建单例的方式是独一无二的,但是,很容易造成命名空间的污染,大型项目中,多个人协同开发,一不小心就会将你创建的单例给覆盖掉了,还得不断的加以限制,所以不建议使用全局变量的这种方式创建单例,可以使用命名空间来减少全局变量的使用,通过命名空间和闭包的结合,就可以实现无类的单例模式实现了,也就是上的几种代码(这里大家可能会觉得很奇怪,我明明说了上面的传统方式在JavaScript中不适用,为何在这又说就是以上代码的实现就是JavaScript中的单例模式,这里主要讲的其实是设计思路,与代码无关,由于JavaScript是无类的语言,所以无法写出类语言那种的形式,只是做了大概的模拟,本质上的代码实现还是JavaScript代码,仔细观察也不难看出,上面的代码就是用到了闭包创建私有变量的形式来保存构建好的单例的,也使用了原型链来模拟类中的静态方法,所以大家不必疑惑)
通用惰性单例
这种算是较为贴合实际情况的,将创建节点的逻辑抽离以及管理单例的逻辑也抽离出来,通过将创建节点的逻辑传入管理单例的构造函数中,从而实现单例模式,代码如下
let getSingle = function (fn) {
let result = null
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
}
const loginLayer = getSingle(createLoginLayer)
document.getElementById("loginButton").onclick = function () {
let loginLayerAsc = loginLayer()
loginLayerAsc.style.display = 'block'
}
大家不妨试试最后的结果,我们根据上面的代码在来实现动态第三方页面的加载
let getSingle = function (fn) {
let result = null
return function () {
return result || (result = fn.apply(this, arguments))
}
}
let createSingleIframe = getSingle(function () {
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe
})
document.getElementById("loginButton").onclick = function () {
let loginLayerAsc = createSingleIframe()
loginLayerAsc.src = 'https://www.baidu.com/'
}
最终结果很意外,被拦截拒绝了,哈哈哈,不过说明单例的实现是成功的,不管点击多少次登录,都有且仅会创建一次模块
以上就是关于单例模式的所有的介绍,包含了许多闭包,需要对闭包理解透彻之后才能完全读懂单例模式的代码
内容摘自《JavaScript设计模式与开发实践》· 曾探 · 著