单例模式是创建对象最简单的方式。单例模式的定义 是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
在JavaScript开发中,单例模式的用途同样非常广泛。试想,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
实现单例模式的要点是:用flag量来表示是否为某类创建过对象,下次调用的时候,如该对象已经创建,就返回已经创建的对象实例,否则创建一个对象实例并返回。
class Person{
constructor(name){
this.name=name;
this.instance=null;
}
getName(){
console.log(this.name);
}
}
Person.getInstance=function(name){
this.instance=!this.instance?new Person(name):this.instance;
return this.instance;
}
let a=Person.getInstance('djtao');
let b=Person.getInstance('dangjingtao');
console.log(a===b) // true
在上面的例子中,Person类实例由始自终只有一个。保证了它的独一无二。
但代码存在一个问题,就是"不透明",使用者必须通过通过 getInstance
调用之.
说到对象,基本都用person为例子。现在来看点实用的。
比如,我要在页面中创建一个唯一的div节点。同样也可以使用 new
关键字。
const CreateDiv=(function(){
let instance;
class CreateDiv{
constructor(html){
if(instance){
return instance;
}
this.html=html;
this.init();
return instance=this;
}
init(){
let div=document.createElement('div');
div.innerHTML=this.html;
document.body.appendChild(div);
}
}
return CreateDiv;
})();
const a=new CreateDiv('djtao');
const b=new CreateDiv('dangjingtao');
console.log(a===b);//true
此时页面创建了只有一个新div且只显示 dangjingtao
。
在这段看似炫技的代码中,解决了不透明的问题。但又带来了新的问题。
为了把instance封装,上述采用匿名函数自执行和闭包。并且用使此函数返回了真正的构造函数。增加了复杂度,读起来也不舒服。
CreateDiv实际上做了两件事情,一个是创建对象,一个是保证只有一个对象。从单一指责原则来说,这不是一个好的做法。假如项目后期我不再需要一个单例,而需要用它来创造N个div,那就痛苦了。
单一职责原则是设计模式的重要原则: 应该有且仅有一个原因引起类的变更(There should never be more than one reason for a class to change) 单一职责原则为我们提供了一个编写程序的准则,要求我们在编写类,抽象类,接口时,要使其功能职责单一纯碎,将导致其变更的因素缩减到最少。 如果一个类承担的职责过多,就等于把这些职责耦合在一起。一个职责的变化可能会影响或损坏其他职责的功能。而且职责越多,这个类变化的几率就会越大,类的稳定性就会越低。 在软件开发中,经常会遇到一个功能类T负责两个不同的职责:职责P1,职责P2。现因需求变更需要更改职责P1来满足新的业务需求,当我们实现完成后,发现因更改职责P1竟导致原本能够正常运行的职责P2发生故障。而修复职责P2又不得不更改职责P1的逻辑,这便是因为功能类T的职责不够单一,职责P1与职责P2耦合在一起导致的。
设想有这样一个工厂方法,能把普通的类转化成单例:
class CreateDiv{
constructor(html){
this.html=html;
this.init();
}
init(){
let div=document.createElement('div');
div.innerHTML=this.html;
document.body.appendChild(div);
}
}
const Proxy=(function(){
let instance=null;
return function(html){
if(!instance){
instance=new CreateDiv(html)
}
return instance;
}
})();
const a=new Proxy('dangjingtao);
const b=new Proxy('djtao');
console.log(a===b);//true
有了Proxy,CreateDiv就变成了一个简单的类。显然比上一段易读易用。
上面代码都是基于类创建的单例。JavaScript并非是一个真正有"类"的语言。在实践中,有时并不需要做这种脱裤子放屁的事。(笔者注:本人还是比较喜欢做这种事。)
全局变量就是一个单例。js中声明全局变量还是非常简单的。比如在jquery中时 $
。然而全局变量是js最广受诟病的缺点之一。如何避免?
用属性来取代全局变量,比如用 a.b
来取代 b
。还可以动态地创建:
let App={};
App.nameSpace=function(name){
const parts=name.split('.');
let current=App;
for(let attr in parts){
if(!current[parts[attr]]){
current[parts[attr]]={};
}
}
}
App.nameSpace('event');
App.nameSpace('dom.style');
这段代码等效于:
var App={
event:{},
dom:{
style:{}
}
}
类似jQuery之类的库,用的就是闭包来规避全局变量的问题。它把所有的代码装在一个自执行的函数中。只暴露一些和外界通讯的接口。
const user=(function(){
const _name='djtao';
const _job='programer';
return {
getUserInfo:function(){
return `${_name}-${_job}`;
}
}
});
惰性是单例模式的重点。有用的程度超过了想象。
使用bootstrap做modal模态框时。我们总是会在html页面全局写段又臭又长的弹框:
<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel">Modal title</h4>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
正常状态下,一个按钮对应且只能对应一个模态弹框。
但我使用的时候,也许只是做其它操作,压根不想去点这玩意。也就是说,这段DOM代码被浪费了。
在设计代码时,应该考虑:用一个变量来判断是否创建过。你可以找寻这个dom节点。如果找不到,就创建。于是你可能会这么写:
const createModal=(function(){
let modal;
return function(){
if(!modal){
modal=document.createElement('div');
modal.innerHTML=`...`;
modal.style.display='none';
document.body.appendChild(modal);
return modal;
}
}
});
这是一个可用的惰性单例。但仍然是违反单一职责原则的。假设你辛苦做完项目后,嬗变的需求经理哪天又跟你说:我不要modal了,全部改为iframe。那你就得把相同的逻辑再copy一遍。把创建modal改为创建iframe。此刻你的心里想必是骂妈卖批(mmp)的吧。
设想,惰性单例无非就是这么一个逻辑,用伪代码表述就是:
声明对象obj
if(obj存在){
obj=xxx
}
这段逻辑是可以从业务上抽离出来的。以下便是本文的精华:
const getSingle=function(fn){
let result;
return function(){
return result||(result=fn.apply(this,arguments));
}
}
有了这个方法,随便你怎么创建多少个方法。都是惰性的单例了。
// 业务代码
const createIframe=function(){
let modal = document.createElement('div');
modal.innerHTML = `...`;
modal.style.display = 'none';
document.body.appendChild(modal);
return modal;
}
const createMMP=function(){
// ...
}
const iframe=getSingle(createIframe);
const mmp=getSingle(createMMP);
当有人说起单例模式能干嘛时,如果你的反应是"获取对象",那其实是片面的。现在再看一个例子。
假如你在设计一个前端框架,遇到常见的场景:当ajax请求渲染一个列表(对应方法是render),你给它们每个item绑定click事件(bindEvent)。应该怎么做?
那就痛苦了吧!
当然解决思路不止一种。单例模式可以很优雅解决该问题:
const bindEvent=getSingle(function(){
xxx.onclick=function(){
// todo
};
return true;
});
const render=function(){
// 渲染数据
bindEvent();
}
render();
render();
render();
无论你怎么执行,最后只绑定了一次。