本文由《JavaScript设计模式与开发实践》–曾探著总结而来。
应用场景
某个作用域内(如全局作用域)只需要一个具备功能的对象。例如全局的window,登录框等等。总之,就是在顶级的作用域中用来掌控顶级信息的对象。这样的对象是唯一的,适合用单例模式来创建。
创建方式
思路:包括两点,
- 首先要创建这样的对象。创建对象可以用ES6语法中的类(或ES5的构造函数),通过new 类名()来创建,也可以用对象字面量语法创建。
- 其次要保证对象唯一。保证唯一可以采用只调用一次类的构造函数,再次调用直接返回已创建好的对象。当然,对象字面量语法创建的对象本身就是唯一的。
方式一:getInstance()方法返回单例
利用类来创建,但不通过new方式调用,而是调用专门创建单例的方法。
// 例如创建一个存储数据的Store对象
// 在类上挂载静态属性来区分实例是否创建
class Store {
constructor(props) {}
Store.instance = null; // 在类上挂载静态属性instance来判断是否已经创建了实例
Store.getInstance = function(props) {
if(!this.instance) {
this.instance = new Store(props);
}
return this.instance;
}
}
var store1 = Store.getInstance(props1);
var store2 = Store.getInstance(props2);
store1 === store2; // true
// 同样可以通过闭包变量来区分实例是否创建
class Store {
constructor(props) {}
Store.getInstance = (function() {
// 声明一个闭包变量,用来判断实例是否已经创建
var instance = null;
return function(props) {
if(!instance) {
instance = new Store(props);
}
return instance;
}
})()
}
这种方式在思路上很清晰,但却存在着“不透明”的问题。使用的是Store.getInstance()
来创建实例,而并非通用的new调用方式。另外,创建对象和保证唯一两种功能耦合在一起,不符合“函数单一职责”的原则,不利于代码复用和扩展。
方式二:new调用返回单例
使用类来创建,但用new调用,符合通常的创建实例方式。既然是用new调用,那么创建实例和判断唯一性就会在构造函数中进行。
class Store {
Store.instance = null;
constructor(props) {
if(Store.instance) {
return Store.instance;
}
// 执行一系列初始化this的逻辑;
Store.instance = this;
return Store.instance;
}
}
这种方式解决了“透明”问题,但创建实例和保证唯一仍然耦合在一起。
方式三:代理模式,分离创建实例和保证唯一的耦合,将保证唯一的功能集成到代理函数中
利用代理模式,将创建实例的功能和保证唯一的功能分离开,各自集成在不同的函数中,从而可以使代码利于复用。这样创建实例的时候可以按正常方式创建很多实例,当需要创建单例时,用代理函数调用即可。
// 创建实例的功能由Store负责,保证唯一的功能由getStoreInstance函数负责;
class Store {
constructor(props) {}
}
// getStoreInstance函数负责代理保证唯一的功能;
const getStoreInstance = (function() {
var instance = null;
return function(props) {
if(!instance) {
intance = new Store(props);
}
return instance;
}
})()
对象字面量语法创建单例
对象字面量语法创建单例很方便,但容易污染全局作用域。由两种方式来减少污染:
- 把单例置于命名空间中
- 用闭包将单例变为私有变量
管理单例的逻辑
由上述代码发现,管理单例的逻辑(即如何保证唯一的逻辑)基本上类似于如下代码:
var instance = null;
if(!intance) {
// 创建实例并赋予instance
}
return instance;
惰性单例
有的时候不需要页面加载时就创建单例,而是响应用户行为,或者在页面空闲时创建单例,这样会使页面性能提高。例如,登录框的创建,用户可能并不想登录,只是想浏览页面,此时的登录框适合用惰性单例的方式去创建,在用户点击登录按钮时创建登录框。
// html
<body>
<button id="login">登录</button>
</body>
// script
<script>
// getInstance函数用来代理单例的管理,接收任何创建对象的方法,返回该单例对象。这样不管创建什么对象,都可以用getInstance函数来管理单例。
const getInstance = (function() {
var instance = null;
// fn为创建DOM节点的函数,但不能是通过new调用的实例化函数;
return function(fn) {
var args = Array.prototype.slice.apply(arguments, 1); // args保存除fn外的其他参数;
if(!instance) {
instance = fn.apply(this, args);
}
return instance;
}
})()
const createLogin = function() {
var div = document.createElement('div');
div.innerHTML = '请登录';
div.style.display = 'none';
document.body.appendChild(div);
return div;
}
document.getElementById('login').addEventListener('click', function() {
var div = getInstance(createLogin);
div.style.display = 'block';
});
</script>
单例模式的其他应用
在单例模式的管理逻辑中,实际上不只是创建单例对象,所有只需要执行一次的行为逻辑,都可以利用单例模式来管理。如jQuery中只绑定一次事件的one()方法,即可以用单例模式去实现。