通用的惰性单例一
假设我们是WebQQ 的开发人员(网址是web.qq.com),当点击左边导航里QQ 头像时,会弹出一个登录浮窗(如图4-1 所示),很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。
第一种解决方案是在页面加载完成的时候便创建好这个div 浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮的时候,它才开始显示:
//html
//js
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';
};
这种方式有一个问题,也许我们进入WebQQ 只是玩玩游戏或者看看天气,根本不需要进行登录操作,因为登录浮窗总是一开始就被创建好,那么很有可能将白白浪费一些DOM节点。现在改写一下代码,使用户点击登录按钮的时候才开始创建该浮窗:
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';
};
虽然现在达到了惰性的目的,但失去了单例的效果。当我们每次点击登录按钮的时候,都会创建一个新的登录浮窗div。虽然我们可以在点击浮窗上的关闭按钮时(此处未实现)把这个浮窗从页面中删除掉,但这样频繁地创建和删除节点明显是不合理的,也是不必要的。也许读者已经想到了,我们可以用一个变量来判断是否已经创建过登录浮窗,这也是本节第一段代码中的做法:
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';
};
但是我们发现它还有如下一些问题。
- 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer
对象内部。 - 如果我们下次需要创建页面中唯一的iframe,或者script 标签,用来跨域请求数据,就
必须得如法炮制,把createLoginLayer 函数几乎照抄一遍:
var createIframe = (function () {
var iframe;
return function () {
if (!iframe) {
iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
return iframe;
}
})();
我们需要把不变的部分隔离出来,先不考虑创建一个div 和创建一个iframe 有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象:这些逻辑被封装在getSingle函数内部,创建对象的方法fn 被当成参数动态传入getSingle 函数:
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
接下来将用于创建登录浮窗的方法用参数fn 的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr 等。之后再让getSingle 返回一个新的函数,并且用一个变量result 来保存fn 的计算结果。result 变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果result 已经被赋值,那么它将返回这个值。代码如下:
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';
};
在这个例子中,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能,看起来是一件挺奇妙的事情。
通用的惰性单例二
这种单例模式的用途远不止创建对象,比如我们通常渲染完页面中的一个列表之后,接下来要给这个列表绑定click 事件,如果是通过ajax 动态往列表里追加数据,在使用事件代理的前提下,click 事件实际上只需要在第一次渲染列表的时候被绑定一次,但是我们不想去判断当前是否是第一次渲染列表,如果借助于jQuery,我们通常选择给节点绑定one 事件:
var bindEvent = function () {
$('div').one('click', function () {
alert('click');
});
};
var render = function () {
console.log('开始渲染列表');
bindEvent();
};
render();
render();
render();
如果利用getSingle 函数,也能达到一样的效果。代码如下:
var getSingle = function (fn) {
var result;
return function () {
return result ? result : result = fn.apply(this, arguments);
}
}
var bindEvent = getSingle(function () {
document.getElementById('loginBtn').addEventListener("click", function () {
alert('click');
});
return true;
});
var render = function () {
console.log('开始渲染列表');
bindEvent();
};
render();
render();
render();