单例模式的定义:保证一个类只有一个实例,并且向外提供一个访问它的全局访问点。
在JavaScript中,我们可能会使用全局变量来当作单例模式,比如let a = {},这当然可以,既满足了只有一个实例,又满足全局访问点,但是这样容易造成命名空间污染。在大型项目中如果不加管理,很容易出现很多这样变量,也很容易被不小心覆盖。
对于全局变量我们可以使用闭包封装私有变量:
let user = (function(){
let _name = 'felix',
_age = 19;
return {
getUserInfo:functino (){
return _name + '-' + _age;
}
}
})()
我们约定私有变量_name和_age,她们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这样就避免了全局变量的污染。
惰性单例
惰性单例指的是只有在我们需要的时候才创建对象实例。惰性单例是单例模式的重点,因为他很好用!
下面代码展示的是:点击头像弹出登录窗口。
let creatLoginLayer = (function(){
let div;
return function(){
if(!div){
div = document.creatElement('div');
div.innerHTML = '我是登录弹窗';
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})()
document.getElementById('loginId').onclick = function(){
let loginLayer = creatLoginLayer();
loginLayer.style.display = 'block';
}
在这段代码中存在两个问题
- 其实这段代码违反单一职责原则,创建对象和管理单例的逻辑都放在了一起。
- 如果我们还要创建注册/或者其他元素,就要把这个逻辑复制一遍吗?我相信很多人都这样懒省事做过,比如看下面创建
iframe的例子。
var createIframe= (function(){
var iframe;
return function(){
if ( !iframe){
iframe= document.createElement( 'iframe' );
iframe.style.display = 'none';
document.body.appendChild( iframe);
}
return iframe;
}
})();
其实我们可以把不变的部分抽离出来。
let obj;
if(!obj){
obj = xxx;
}
现在我们就把刚才的例子中不变的抽出来。
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
接下来我们来实现创建登录浮窗。我们把登录浮窗的方法当作getSingle的参数fn传进去,我们不仅可以传入creatLoginLayer,还可以传入creatIframe、creaetScript等。之后再让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';
};
总结
在单例模式中,我们用到了闭包和高阶函数,也在实现单例模式的过程中,把控制实例只实现一个的方法和逻辑相拆分,也就是创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。
参考
JavaScript设计模式与开发实践