JavaScript设计模式之单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有些对象往往只需要一个,比如:线程池、全局缓存、浏览器中的 window 对象等。在 Javascript 开发中,单例模式的用途同样非常广泛,比如做登录弹框,它在当前页面是唯一的,无论单击多少次,都只会创建一次,这就非常适合用单例模式来创建。
- 单例模式
var Singleton = function(name) {
this.name = name;
}
Singleton.prototype.getName = function() {
console.log(this.name);
}
Singleton.getInstance = (function() {
var instance = null;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
var a = Singleton.getInstance('JavaScript');
var b = Singleton.getInstance('vue.js');
console.log(a === b); // true
代码通过 Singleton.getInstance 来获取 Singleton 类的唯一对象, 这种实现相对简单,但也存在一个问题,增加了这个类的“不透明性”,Singleton 类的使用者必须知道这是一个单例类,跟以往通过 new xxx 的方式来获取对象不同。
- 透明的单例模式
实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。
var Singleton = (function() {
var instance = null;
var Singleton = function(name) {
this.name = name;
if (instance) {
return instance;
}
return instance = this;
}
Singleton.prototype.getName = function() {
console.log(this.name);
}
return Singleton;
})();
var a = new Singleton('JavaScript');
var b = new Singleton('vue.js');
console.log(a === b); // true
- 用代理实现单例模式
var Singleton = function(name) {
this.name = name;
}
Singleton.prototype.getName = function() {
console.log(this.name);
}
var ProxySingleton = (function() {
var instance = null;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
var a = new ProxySingleton('JavaScript');
var b = new ProxySingleton('vue.js');
console.log(a === b); // true
- 惰性单例
惰性单例是指在需要的时候才创建对象实例。惰性单例这种技术在实际开发中非常有用,即只有在调用的时候才被创建,而不是在页面的加载好的时候就创建。
Singleton.getInstance = (function() {
var instance = null;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
不过这种基于类的单例模式在 JavaScript 中并不适用,下面我们以登录弹框为例,介绍与全局变量结合实现惰性的单例。
<script>
var loginMsgbox = (function() {
var div = document.createElement('div');
div.innerHTML = '登录框';
div.style.display = 'none';
document.body.appendChild(div);
return div;
})();
document.getElementById('loginBtn').onclick = function() {
loginMsgbox.style.display = 'block';
}
</script>
这种方式会有一个问题,如果用户进入压根不需要进行登录操作,提前创建好 DOM 节点,有点浪费资源。
<button id="loginBtn">登录</button>
<script>
var loginMsgbox = function() {
var div = document.createElement('div');
div.innerHTML = '登录框';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
document.getElementById('loginBtn').onclick = function() {
var login = loginMsgbox();
login.style.display = 'block';
}
</script>
现在虽然达到了惰性目的,但失去了单例的效果。加个变量来判断是否已经创建过登录弹框。
<button id="loginBtn">登录</button>
<script>
var loginMsgbox = (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 login = loginMsgbox();
login.style.display = 'block';
}
</script>
- 通用的惰性单例
上面我们完成了一个可用的惰性单例,但是也存在一些问题
- 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在 loginMsgbox 对象内部。
- 如果我们下次需要创建页面中唯一的一个注册弹框,就又得把 loginMsgbox 代码重新复制一遍。
需要达到通用,就得经过封装,需要把不变的部分隔离出来。
<button id="loginBtn">登录</button>
<button id="registerBtn">注册</button>
<script>
// 封装单例模式
var getSingle = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
}
}
// 登录
var loginMsgbox = function() {
var dom = document.createElement('div');
dom.innerHTML = '登录弹框';
dom.style.display = 'none';
document.body.appendChild(dom);
return dom;
}
var createLoginMsgbox = getSingle(loginMsgbox);
document.getElementById('loginBtn').onclick = function() {
var login = createLoginMsgbox();
login.style.display = 'block';
}
// 注册
var registerMsgbox = function() {
var dom = document.createElement('div');
dom.innerHTML = '注册弹框';
dom.style.display = 'none';
document.body.appendChild(dom);
return dom;
}
var createRegisterMsgbox = getSingle(registerMsgbox);
document.getElementById('registerBtn').onclick = function() {
var register = createRegisterMsgbox();
register.style.display = 'block';
}
</script>
单列模式用途远不止创建对象,比如给 li 列表绑定 click 事件,如果是通过 ajax 动态往列表里追加数据,在使用事件代理的前提下(事件委托不会存在),click 事件实际上只需要在第一次渲染列表的时候被绑定一次,但是又不想去判断当前是否是第一次渲染列表。
<div id="div1">div1</div>
<script>
// 封装单例模式
var getSingle = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
}
}
// 绑定事件
var bindEvent = getSingle(function() {
console.log('绑定事件')
document.getElementById('div1').addEventListener = function() {
alert('click');
}
return true;
})
var render = function() {
console.log('渲染列表');
bindEvent();
}
render();
render();
</script>