js设计模式学习一(单例模式)
写在最前
为什么会有设计模式这样的东西存在,那是因为程序设计的不完美,需要用设计模式来弥补设计上的缺陷,那立马估计会有童鞋问,既然设计的不完美,那就换个完美点的语言,首先,没有绝对完美的语言存在,其次,借鉴下前辈说的话,灵活而简单的语言更能激发人们的创造力,所以生命力旺盛,这也能够解释,近些年来前端发展的如此迅速的原因吧。
ps一段,自从开始正儿八经深入学习前端已经有一年多左右了,当时定的一个看书目标就是最初的是dom入门,之后是高三书和犀牛书,截止到现在这三本基本都算看完了,犀牛书后续的一些章节还没有完全看完,在上个工作的过程中还算是比较忙的(可能是外包项目比较多),中间有准备了面试,现在是在大厂了,发现要学的实在是太多了,本厂的框架一堆一堆的,哎,慢慢来吧,基础是一样的区别是实现的思想。
看,这是我当时列的书单
语言精粹这个比较薄,是经常需要翻看,一下看完也记不牢,设计模式这本是汤姆大叔翻译的,普遍反映有点晦涩,在技术群里有推荐说腾讯大神曾探写的(JavaScript设计模式与开发实践)不错,讲的比较容易理解,就买了一本,看了前面三章,讲js的面向对象,原型以及闭包的,是设计模式的基础,觉得还不错。现在就打算根据看书写下设计模式的一系列的博客,水平不高,求指正,求交流。
正文:设计模式--单例模式
单例模式的定义是,保证一个类有且仅有一个实例,并提供一个访问它的全局访问点。
这个模式是常用的模式了,如果结合业务来讲的话,就应该是固定的功能,每次调用都没什么多大变化的,比如,弹窗的提示,登录功能的浮窗,
1,实现单例模式
代码如下
var single = function (name) { this.name = name; this.instance = null; }; single.prototype.getName = function (){ alert(this.name); }; single.getInstance = function (name) { if( !this.instance ){ this.instance = new single(name); } return this.instance; }; var a = single.getInstance('sev1'); var b = single.getInstance('sev2'); alert( a === b);
我们通过 single.getInstance 来获取single类的唯一对象,这种方式相对简单,但有一个问题,就是增加了这个类的“不透明性”,single类的使用者必须知道这是个单例类,跟以往通过new XXX的方式获取对象不同,这里是使用single.getInstance来获取对象。
2,透明的单例模式
我们现在的目标是实现一个透明的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。在下边的例子中,我们将使用creatDiv单例类,它的作用是负责页面中创建唯一的div节点,代码如下:
var creatDiv = (function () { var instance; var creatDiv = function (html) { if(instance){ return instance; } this.html = html; this.init(); return instance = this; }; creatDiv.prototype.init = function () { var div = document.createElement('div'); div.innerHTML = this.html; document.body.appendChild(div); }; return creatDiv; })(); var a = new creatDiv('sev1'); var b = new creatDiv('sev2'); alert( a === b );
虽然现在完成了一个透明的单例类的编写,但它同样有一些缺点。
为了把instance封装起来,使用了自执行函数和闭包,并且让这个匿名函数返回真正的单例构造方法,这增加了一些程序的复杂度,阅读起来也不很舒服,
观察下现在的构造函数,
var creatDiv = function (html) { if(instance){ return instance; } this.html = html; this.init(); return instance = this; };
这个函数负责了两个事情,第一,创建对象并执行初始化函数,第二,保证只有一个对象,虽然还没有接触到 “单一职责原则”的概念,就这个构造函数来说,这是一种不好的做法。
假设我们某天需要利用这个类,在页面中创建千千万万的div,即要让这个类从单例类变成一个普通的 可产生多个实例的类,那我们必须改写creatDiv函数,把控制唯一对象的那一段去掉,这种修改会给我们带来不必要的烦恼。
3,用代理实现单例模式
现在我们引入代理的方式来解决上面的问题,
首先我们把负责管理单例的代码移除出来,使它成为一个普通的创建div的类,
var creatDiv = function () { this.html = html; this.init(); }; creatDiv.prototype.init = function () { var div = document.createElement('div'); div.innerHTML = this.html; document.body.appendChild(div); };
接下来引入代理类,
var ProxySingletonCreatDiv = (function () { var instance; return function (html) { if(!instance){ instance = new creatDiv( html ); } return instance; } })(); var a = ProxySingletonCreatDiv('sev1'); var b = ProxySingletonCreatDiv('sev2'); alert( a === b );
通过引入代理类的方式,完成了一个单例模式的编写,跟之前不同是把负责单例的逻辑和实现创建div的类分开了,这个是缓存代理的应用之一,后续还会有关于代理的一些应用。
4,JavaScript中的单例模式
前面的几种单例模式的实现,更多的是传统面向对象语言中的实现,单例对象从类中创建而来,在以类为中心的语言中,这是很自然的做法,比如在java中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来。
但是javascript其实是一门无类的语言,在js中创建对象的方法非常简单,既然我们需要一个唯一的对象,为什么还需要先创建一个类呢,传统的单例模式并不适用与JavaScript。
全局变量不是单例模式,但是在js中我们经常会把全局变量当成单例模式来用,
例如 var a = {};
当用这种方式创建对象a的时候,对象a是独一无二的,如果a变量被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的,这样就满足了单例模式的两个条件。
作为一名js开发者,全局变量有多困扰就不用详细说了,连JavaScript的创造者本人也承认全局变量是设计失误,在es6中已经有对应的处理方式了。
全局变量的污染也是有解决方式的,
4.1,使用命名空间
只能减少,不能杜绝。例如:
var namespace = { a:function(){ alert(1); }, b:function(){ alert(2); } }
4.2,使用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信。
var user = (function () { var _name = 'sven', _age = 29; return { getUserInfo : function () { return _name + '-' + _age; } } })();
5,惰性单例
前面我们了解了单例模式的一些实现办法,现在来看下惰性单例。
惰性单例指的是在需要的时候才创建对象的实例,惰性单例是单例模式的重点,这种技术在实际开发中非常有用。就像是一开始的instance 实例在我们调用getInstance 时才被创建,
Singleton.getInstance = (function () { var instance = null; return function (name) { if(!instance){ instance = new Singleton(name); } return instance; } })();
不过这是基于类的单例模式,前面已经说过,这种是不适用于javascript的,结合一个登录弹框的场景,来实现下惰性单例。
第一种解决方案就是,页面加载的时候就创建好,一开始隐藏,点击登录的时候显示出来。
这种方式有个问题,如果用户在这个页面只是想浏览其他内容,不需要登录,那么久浪费了一些dom节点。
如果改写下,在点击登录的时候才开始创建弹窗,
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <button id="btn">登录</button> </body> <script> var createLogin = function () { var div = document.createElement('div'); div.innerHTML = '我是登录弹窗'; div.style.display = 'none'; document.body.appendChild(div); return div; } document.getElementById('btn').onclick = function () { var login = createLogin(); login.style.display = 'block'; } </script> </html>
虽然达到了惰性的目的,但是失去了单例的效果,但我们每次点击都会新创建一个弹窗,显然是不好,即使我们可以在右上设置个将页面删除掉的按钮,但是频繁的创建和删除节点明显是不合理的。
我们可以用变量判断页面是否已经创建弹窗,这也是基于类的单例模式的做法。creatLogin就可以改写成这样
var createLogin = (function () { var div; return function () { if(!div){ div = document.createElement('div'); div.innerHTML = '我是登录弹窗'; div.style.display = 'none'; document.body.appendChild(div); } return div; } })();
6,通用的惰性单例
上一节,我们实现了一个可用的惰性单例,但是他还是有一些问题,这段代码是违反了单一职责原则的,创建弹窗和管理单例的逻辑都在creatLogin对象内部,如果我们需要一个创建iframe的单例,那就要重新将这个函数在写一遍,能不能将管理单例的逻辑提出来呢,功能的实现是单独的函数。
管理单例的逻辑都放到getSingle函数里面,
var getSingle = function (fn) { var result; return function () { return result || (result = fn.apply(this, arguments)); } }
创建弹窗的函数就可以写成这样
var createLogin = function () { var div = document.createElement('div'); div.innerHTML = '我是登录弹窗'; div.style.display = 'none'; document.body.appendChild(div); return div; };
creatSingleLogin 就是个惰性单例的函数
var creatSingleLogin = getSingle(creatLogin);
如有其他实现的,创建iframe等其他的,都可以由getSingle这个来创建。
其实这种单例模式不只是创建对象,比如我们通常渲染完页面的中的一个列表之后,接下来要给这个列表绑定click事件,如果是通过ajax动态网列表里加数据,在使用事件代理的前提下,click事件实际上只需要在第一次渲染列表的时候绑定一次,但是我们不想去判断当前是否是第一次渲染列表,如果借助jquery,我们通常选择给节点绑定one事件:
var bindEvent = function () { $('div').one('click', function () { alert('click'); }) }; bindEvent(); bindEvent(); bindEvent();
虽然函数执行3次,但是绑定事件还是只绑定了一次
用getSingle函数也可以达到一样的效果, var bindEvent = getSingle(function () { document.getElementById('btn').addEventListener( 'click', function () { //原书用的onclick,验证过这个执行几次也是执行一次,改成了事件绑定的模式 console.log('ssss') }); return true; }); bindEvent() bindEvent()
这样的话,getSingle的入参函数必须要有个返回值,所以要 return true。
其实,像这样的场景最好的肯定是事件委托,性能方面也会优化很多。
小结
单例模式是我们学习的第一个模式,我们先学习了传统的单例模式的实现,也了解因为语言的差异性,有更合适的方法在javascript中创建单例,这一章还提到了代理模式和单一职责原则。
在getSingle函数中,实际上也提到了闭包和高阶函数的概念,单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个,更奇妙的是创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才有单例模式的威力。