定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有写对象我们往往只需要一个,比如全局缓存、window对象等。在实际应用场景中也经常用到,登录时的弹窗,无论点击多少次登录按钮,这个弹窗只会被创建一次,那么这个弹窗就适合使用单例模式来使用。
实现单例模式
实现一个标准的单例模式并不复杂,需要使用一个变量来标志当前是否已为某个类创建过对象,如果是,则需要在下一次获取该类的实例时,直接返回之前创建的对象。
var Singleton = function (name) {
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function () {
alert(this.name);
};
Singleton.getInstance = function (name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
};
var a = Singleton.getInstance("sven1");
var b = Singleton.getInstance("sven2");
console.log(a === b); // true
上述代码中,可以通过 Singleton.getInstance 来获取 Singleton 类的唯一对象,但是我们可以看到上述代码增加了类的“不透明性”,使用 Singleton 类的时候必须知道这是一个单例类。
透明的单例模式
现在的目标来实现一个“透明”的单例类,可以像使用任何其他普通类一样。以下实现在页面中创建一个唯一的 div——CreateOnlyDiv 类。
var CreateOnlyDiv = (function () {
var instance;
var CreateOnlyDiv = function (html) {
if (instance) {
return instance;
}
this.html = html;
this.init();
return (instance = this);
};
CreateOnlyDiv.prototype.init = function () {
var div = document.createElement("div");
div.innerHTML = this.html;
document.body.appendChild(div);
};
return CreateOnlyDiv;
})();
var a = new CreateOnlyDiv("div1");
var b = new CreateOnlyDiv("div2");
console.log(a ==== b); // true
现在,完成了一个透明的单例类的编写。但是,为了把 instance 封装起来,使用了自执行的匿名函数和闭包,且让这个匿名函数返回真正的 Singleton 构造方法,增加了程序复杂度的同时,阅读也很困难。
CreateOnlyDiv 的构造函数负责了两件事情:
- 创建对象和执行初始化
init方法 - 保证只有一个对象
针对扩展性来说,若哪天需要创建多个 div 时,就必须修改 CreateOnlyDiv 的构造函数,把控制唯一对象的那一段去掉,如此,这段代码的扩展性并不好。
用代理实现单例模式
引入代理类的方式,来解决以上的问题。
首先,在 CreateOnlyDiv 构造函数中,把负责单例的代码移除,变成一个普通的创建 div 的类—— CreateDiv。
var CreateDiv = function (html) {
this.html = html;
this.init();
};
CreateDiv.prototype.init = function () {
var div = document.createElement("div");
div.innerHTML = this.html;
document.body.appendChild(div);
};
然后,引入代理类 proxySingletonCreateDiv。
var ProxySingletonCreateDiv = (function () {
var instance;
return function (html) {
if (!instance) {
instance = new CreateDiv(html);
}
return instance;
};
})();
var a = new ProxySingletonCreateDiv("sven1");
var b = new ProxySingletonCreateDiv("sven2");
console.log(a === b); // true
通过引入代理类的方式,完成了单例模式的编写。跟之前不同的是,将处理单例的逻辑写到了代理类 ProxySingletonCreateDiv 中,这样 CreateDiv 就是一个普通的类,他们结合可以达到单例模式的效果。
JavaScript 中的单例模式
前面提到的几种单例模式的实现,更多的是传统面向对象语言中的实现,单例对象从“类”中创建而来。但是,对于 JavaScript 来说,创建对象的方法非常简单。
单例模式的核心是 确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但是在 JavaScript 中,经常把全局变量当成单例来使用。
var a = {}
使用这种方式创建的对象 a,是独一无二的。如果 a 变量被声明在全局作用域下,则可以在代码的任何位置使用这个变量,这样就满足了单例模式的两个条件。
但是全局变量存在很多问题,很容易造成命名空间污染。在大中型项目中,如果不加限制和管理,程序中可能会有这样的变量。而且 JavaScript 中的变量也很容易被不小心覆盖。因此,作为普通的开发者,我们有必要尽量减少全局变量的使用,即使要使用也需要将它的污染降到最低。
1 使用命名空间
最简单的方法是使用对象字面量的方式:
var namespace1 = {
a: function() {
console.log(1)
},
b: function() {
console.log(2)
}
}
把 a 和 b 都定义为 namespace1 的属性,可以减少变量和全局作用域打交道的机会。
2 使用闭包封装私有变量
将变量封装在闭包的内部,只暴露一些接口跟外界通信。
var user = (function() {
var __name = 'sven',
__age = 29
return {
getUserInfo: function() {
return __name + '-' + __age
}
}
})()
用下划线来约定私有变量 __name 和 __age ,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量,避免了对全局的命令污染。
惰性单例
惰性单例指的是在需要的时候才创建对象实例,在本文开始时 instance 实例对象就是在我们调用 Singleton.getInstance 的时候才被创建,而不是在页面加载好的时候就创建。 但是这是基于“类”的单例模式。我们以登录弹框为例来介绍与全局变量结合实现惰性的单例。
1 页面加载完创建
此时,这个弹窗一开始肯定是隐藏状态,用户点击登录按钮时才开始显示:
<button id="loginBtn">登录</button>
<script>
var loginLayer = (function() {
var div = document.createElement('div')
div.innerHTML = '我是登录弹窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
})()
document.getElementById('loginBtn').onclick = function() {
loginLayer.style.display = 'block'
}
</script>
2 点击登录时再创建
我们进入一个系统,可能只是看看,并不需要登录,如果登录弹窗一开始就被创建后,可能会浪费一些DOM节点。
<button id="loginBtn">登录</button>
<script>
var createLoginLayer = function() {
var div = document.createElement('div')
div.innerHTML = '我是登录弹窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createLoginLayer()
loginLayer.style.display = 'block'
}
</script>
现在达到了惰性的目的,但是失去了单例的效果,因为每次点击登录按钮的时候都会创建一个新的登录窗口div。虽然可以在点击关闭时,将这个弹窗从页面中删除掉,但这样频繁创建和删除节点明显是不合理的,也是不必要的。
3 用一个变量来判断是否已经创建过
<button id="loginBtn">登录</button>
<script>
var createLoginLayer = (function() {
var 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() {
var loginLayer = createLoginLayer()
loginLayer.style.display = 'block'
}
</script>
通用的惰性单例
上一节完成了一个可用的惰性单例,但是它还有一些问题:
- 违反单一职责原则,创建对象和管理单例的逻辑都在
createLoginLayer内部 - 如果下次需要创建页面中唯一的其他元素,就需要如法炮制,把
createLoginLayer函数几乎照抄一遍。
因此,需要将管理单例的逻辑从原来的代码中抽离出来,这部分逻辑封装在 getSingle 函数内部,创建对象的方法 fn 当做参数传入。
var getSingle = function(fn) {
var result
return function() {
return result || (result = fn.apply(this, arguments))
}
}
接下来将创建登录弹窗的方法用参数 fn 的方式传入 getSingle。
var createLoginLayer = function() {
var div = document.createElement('div')
div.innerHTML = '我是登录浮窗'
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'
}
至此,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能。
总结:单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。
最后说一句
如果这篇文章对您有所帮助,或者有所启发的话,帮忙点赞关注一下,您的支持是我坚持写作最大的动力,多谢支持。
同系列文章
- JavaScript 设计模式之单例模式
- JavaScript 设计模式之策略模式
- JavaScript 设计模式之代理模式
- JavaScript 设计模式之迭代器模式
- JavaScript 设计模式之发布-订阅模式
- JavaScript 设计模式之命令模式
- JavaScript 设计模式之组合模式
- JavaScript 设计模式之模板方法模式
- JavaScript 设计模式之享元模式
- JavaScript 设计模式之职责链模式
- JavaScript 设计模式之中介者模式
- JavaScript 设计模式之装饰者模式
- JavaScript 设计模式之状态模式
- JavaScript 设计模式之适配器模式