定义
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象。在JavaScript开发中,单例模式的用途同样非常广泛,当我们点击登录按钮时,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单词多少次,这个浮窗只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
实现单例模式
要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
代码如下:
let Singleton=function(name){
this.name=name
}
Singleton.prototype.getName=function(){
return this.name
}
//用一个变量标志当前这个类是否被创建
Singleton.instance=null
Singleton.getInstance=function(name){
if(!this.instance){
this.instance=new Singleton(name)
}
return this.instance
}
let a=Singleton.getInstance("a")
let b=Singleton.getInstance("b")
console.log(a===b)//true
正常来说,一个类每次实例化生成的对象是不同的,在上述代码中使用getInstance获取实例化生成的对象并赋值给a和b,打印true说明单例模式成果。
也可以不把生成的实例化对象instance挂载到类的属性上。
Singleton.getInstance=(function(){
let instance
return function(name){
if(!instance){
instance=new Singleton(name)
}
return instance
}
})()
用IIEF创建一个闭包,把instance存储在闭包中,每次执行getInstance时进行判断是否已经实例化过。
透明的单例模式
使用getInstance来获取Singleton类的唯一对象,这种方式相对简单,但是增加了这个类的不透明性。
Singleton类的使用者必须知道这是一个单例类,跟以往通过new xxx的方式来获取对象有所不同,如果要实例化对象,要使用getInstance的方法,故要实现一种透明的单例模式
现有一个需求,使用createDiv单例类,它的作用是在页面中创建唯一的div节点。
//createDiv
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(){
console.log('init...')
}
return createDiv
})()
let a=new createDiv('html1')
let b=new createDiv('html2')
console.log(a===b)//true,且只打印了一次'init...'
虽然完成了一个透明的单例类,但存在一些缺点。
let createDiv=function(html){
if(instance){
return instance
}
this.html=html
this.init()
return instance=this
}
在这段代码中,负责了两件事情,第一是创建对象和执行初始化init方法,第二是保证只实例化一个对象。
这违背了设计模式中的“单一职责原则”,即一段代码中负责了两种职责。
假如某天需要利用这个类,在页面中创建千千万万个div,即要让这个类从单例类变成一个普通的可产生多个实例的类,那么就必须改写createDiv构造函数,把控制创建唯一对象那一段代码去掉,这种修改会带来不必要的麻烦。
下面用代理模式实现单例化。
用代理实现单例模式
首先在createDiv构造函数中,把负责管理单例的代码移除,使它成为一个普通的创建div的类:
//代理实现
var createDiv = function (html) {
this.html = html;
this.init();
};
createDiv.prototype.init = function () {
console.log("init...");
};
接下来引入代理类proxySingletonCreateDiv:
var ProxySingletonCreateDiv = (function () {
let instance;
return function (name) {
if (!instance) {
instance = new createDiv(name);
}
return instance;
};
})();
let a=new ProxySingletonCreateDiv('html1')
let b=new ProxySingletonCreateDiv('html2')
console.log(a,b,a===b)//true
通过引入代理类的方式,把负责管理单例的逻辑迁移到了代理类proxySingletonCreateDiv中,这样一来,createDiv就变成了一个普通的类,它与proxySingletonCreateDiv组合起来可以达到单例模式的效果。
惰性单例
惰性单例指的是需要的时候才创建对象实例。
惰性单例是单例模式的重点,在一开始的例子中就运用到了这种技术。instance实例对象总是在我们调用Singleton.getInstance的时候才创建,并不是在页面加载好的时候才创建。
假设有这么一个网站,当点击右上角登录时,会弹出一个登录浮窗,很明显这个浮窗在页面中是唯一的,不可能同时存在两个登录窗口的情况。
假如希望可以在进入页面的时候,这个浮窗节点先不创建,而等用户点击登录时才创建,达到惰性效果,那么:
<body>
<button id="loginBtn">login</button>
<script>
var createLoginLayer = function () {
var div = document.createElement('div')
div.innerHTML = 'i am loginLayer'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
document.getElementById("loginBtn").onclick = function () {
var loginLayer = createLoginLayer()
loginLayer.style.display = 'block'
}
</script>
</body>
这样虽然达到惰性目的,但每次点击登录按钮时都会创建一个新的登录浮窗div。
这里使用单例模式创建浮窗
<body>
<button id="loginBtn">login</button>
<script>
var getSingle = function (fn) {
let result
return function(){
return result||(result=fn.apply(this))
}
}
var createLoginLayer = function () {
div = document.createElement('div')
div.innerHTML = 'i am loginLayer'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
var createSingleLoginLayer=getSingle(createLoginLayer)
document.getElementById("loginBtn").onclick = function () {
var loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block'
}
</script>
</body>
这个例子中,也是把创建实例对象的职责和管理单例的职责放在两个方法里面,两个方法独立互不影响,当他们连在一起时就实现了单例模式的功能。
ES6实现单例
class Singleton{
constructor(name){
this.name=name
this.init()
}
init(){
console.log('init..')
}
}
const getProxySingleton=function(){
let instance
return class{
constructor(name){
if(instance) return instance
instance=new Singleton(name)
return instance
}
}
}
const ProxySinleton=getProxySingleton()
let a=new ProxySinleton('name1')
let b=new ProxySinleton('name2')
console.log(a,b)