我们大部分前端在学习设计模式时使用的都是使用class类来模拟场景,但是在hooks遍地走的今天,大部分时候并不推崇在业务开发中使用类来处理逻辑,更多是使用处理函数式编程。 而函数式编程要怎样合理运用设计模式呢?
定义
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
场景
要实现一个单例模式并不复杂,无非就是使用一个变量来标识是否为某个类创建过对象。试想一下,如果我们需要在一个表格中添加一个绑定事件,而这个表格又是异步获取数据的,我们更希望在它获取完数据render的时候去绑定事件,但我们又不希望每次render都去重新绑定点击事件,只需要在第一次ajax后绑定即可。这时候单例模式就派上用场了。
实现:类开发逐渐过渡到函数式开发
我们先用易于理解的类开发来实现一个单例模式,代码如下:
js
var Singleton = function( name ){
this.name = name;
};
Singleton.prototype.getName = function(){
alert ( this.name );
};
Singleton.getInstance = (function(){
var instance = null;// 是否实例化过的标识
return function( name ){
if ( !instance ){
instance = new Singleton( name );
}
return instance;
}
})();
上面的代码每次获取实例时:
js
const a = Singleton.getInstance('peng1')
const b = Singleton.getInstance('peng2')
console.log(a.getName() === b.getName()) //true
我们已经完成了一个较为标准的单例模式,但是每次类实例化时用的都是自己的方法,这种操作并不透明。 接下来我们来实现一个透明的单例模式,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。 代码如下:
js
var CreateDiv = (function(){
var instance;
var CreateDiv = function( html ){
if ( instance ){
return instance;
}
this.html = html;
this.init();
return instance = this;
};
CreateDiv.prototype.init = function(){
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};
return CreateDiv;
})();
var a = new CreateDiv( 'sven1' );
var b = new CreateDiv( 'sven2' );
console.log(a.html === b.html) //true
现在我们可以像类一样使用单例模式了,但它同样有一些缺点。为了把 instance 封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回,真正的 Singleton 构造方法,这增加了一些程序的复杂度,并且耦合性太高,违反了单一职责性原则。 我们现在尝试将它的两个功能(实例化和创建单一实例)拆开。现在我们通过引入代理类的方式,来解决上面提到的问题。
js
var CreateDiv = function( html ){
this.html = html;
this.init();
};
CreateDiv.prototype.init = function(){
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};
//接下来引入代理类 proxySingletonCreateDiv:
var ProxySingletonCreateDiv = (function(){
var instance;
return function( html ){
if ( !instance ){
instance = new CreateDiv( html );
}
return instance;
}
})();
var a = new ProxySingletonCreateDiv( 'sven1' );
var b = new ProxySingletonCreateDiv( 'sven2' );
console.log(a.html === b.html) //true
这样一来,两个互不关联的功能就拆开了。不过这是基于"类"的单例模式,前面说过,基于"类"的单例模式在 JavaScript 中并不适用。
实现-函数式开发解决实际问题
场景一:当点击按钮时,出现浮窗。要求:只有点击时才实例化浮窗,实现惰性加载。多次点击不要重复销毁和创建浮窗,实现单例模式。 代码如下:
js
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对象内部。 我们将两个职责来进行拆分:
js
// 一个普通的函数创建实例
var createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
};
// 通用的单例函数:
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
//使用
var createSingleLoginLayer = getSingle( createLoginLayer );
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
createSingleLoginLayer得到的是一个函数,这个函数只会创建一次div,基本符合我们的需求。 回到开头:同样的当我们需要在一个异步列表中需要只绑定一次事件时,只需要将绑定事件的函数传入getSingle中得到一个只会执行一次的函数即可。后面即使需要多次渲染列表,绑定事件的函数也不会再次去执行。
js
var bindEvent = getSingle(function(){
document.getElementById( 'div1' ).onclick = function(){
alert ( 'click' );
}
return true;
});
var render = function(){
console.log( '开始渲染列表' );
bindEvent();
};
render();
render();
render();