JavaScript 设计模式——单例模式
1. 什么是单例模式
单例模式 是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
2. 单例模式的实现
2.1 简单实现
用一个变量标志当前是否已经为某类创建过对象,如果是,则下一次获取实例时直接返回之前创建好的对象。
const Singleton = function(name){
this.name = name;
}
Singleton.prototype.getName = function() {
console.log(this.name)
}
// 单例的实现
Singleton.getInstance = (function() {
let instance = null;
return function(name) {
if(!instance) {
instance = new Singleton(name);
}
return instance;
}
})()
// 使用 & 验证
const a = Singleton.getInstance('instance1');
const b = Singleton.getInstance('instance2');
console.log(a === b); // true
这种方式有一个问题,那就是使用 Singleton 类时必须知道它是一个单例类,且不通过 new XXX 的方法来获取对象,一定要使用 Singleton.getInstance 来获取对象。
2.2 透明的单例模式
下面我们创建一个可以直接使用 new XXX 的“透明”的单例类
const Singleton = (function(){
let instance;
// (*)
const Singleton = function(name){
if(instance){ // (**)
return instance
}
this.name = name; //(***)
}
return Singleton;
})();
观察上面的构造函数(*),它实际上负责了两件事情:1. 创建对象(***
) ;2. 保证只有一个对象(**
)。这违反了“单一职责原则”。如果未来要把这个单例类改成普通的可以产生多个实例的类,必须改写 Singleton 构造函数。
2.3 用代理实现的单例模式
我们引入代理类来解决 2.2 中提到的问题
const Singleton = function(name) {
this.name = name;
}
// 引入代理类
const ProxySingleton = (function(){
let instance;
return function(name){
if(!instance){
instance = new Singleton(name);
}
return instance
}
})();
// 使用 & 验证
const a = new ProxySingleton('instance1');
const b = new ProxySingleton('instance2');
console.log(a === b); // true
在此,我们把管理单例的逻辑移到了代理类 ProxySingleton 中,Singleton 只是一个普通的类。
3. JavaScript 中的单例模式
单例模式的核心是确保只有一个实例,并提供全局访问。
JavaScript 是一门无类(class-free)语言,且用 JavaScript 创建对象非常简单。既然我们只要一个唯一的对象,那为它先创建一个“类”显得有些多余 了。
对于全局变量,比如 const a = {}
,它是独一无二的,且提供给全局访问,满足了单例模式的两个条件。
但是全局变量很容易造成命名空间污染。以下几种方式可以相对降低全局变量带来的命名污染。
3.1 使用命名空间
- 直接用对象字面量
const namespace1 = {
a: function() {
alert(1);
},
b: function() {
alert(2);
}
}
- 动态创建命名空间
const MyApp = {};
MyApp.namespace = function(name) {
const parts = name.split('.');
let current = MyApp;
for(let i in parts) {
if(!current[parts[i]){
current[parts[i]] = {}
}
current = current[parts[i]];
}
}
MyApp.namespace('event');
MyApp.namespace('dom.style');
console.dir(MyApp);
/* 结果
{
"event": {},
"dom": {
"style": {}
}
}
*/
3.2 使用闭包封装私有变量
const user = (function(){
let __name = 'Li Hua';
let __age = 18;
return {
getUserInfo: function() {
return __name + '-' + __age;
}
}
})()
4. 惰性单例
惰性单例 指的是在需要时才开始创建对象实例。
如,点击某按钮后弹出登录浮窗,关闭浮窗后,再次点击该按钮弹出之前创建好的浮窗。
const createLoginLayer = (function(){
let div;
return functuin(){
if(!div){
div = document.createElement('div');
div.innerHTML = '登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
}
return div
}
})();
document.getElementById('loginBtn').onclick = function() {
const loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
}
上面的代码仍然有一些问题
- createLayer 既负责创建对象,又负责处理单例逻辑。
- 如果下次要创建唯一的 ifram,那么要创建一个和 createLoginLayer 几乎一样的方法
const createIframe = (function(){
let iframe;
return functuin(){
if(!iframe){
iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
return iframe
}
})();
下面将单例逻辑抽离出来。
const getSingle = function(fn){
let = res;
return function() {
return res || (res = fn.apply(this, arguments));
}
}
const createLoginLayer = function(){
div = document.createElement('div');
div.innerHTML = '登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
}
const createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById('loginBtn').onclick = function() {
const loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
}
如果想在页面第一次渲染的时候为元素绑定事件,也可以用上面的 getSingle
const bindEvent = getSingle(function() {
document.getElementById('click').onclick = function() {
alert('click');
}
return true;
})
var render = function() {
console.log('开始渲染页面');
bindEvent();
}
render();
render();
render();
render()
和 bindEvent()
都分别执行了3次,但对应元素上只绑定了一个事件。