一、创建型设计模式(三大类设计模式)
创建型设计模式 --"创建"说明该类别里面的设计模式就是用来创建对象的,也就是在不同的场景下我们应该选用什么样的方式来创建对象。
1. 单例模式
==单例模式(Singleton)==:确保一个类只有一个实例,提供一个全局访问点。
- ==标准==单例实现通常要实现一个单例模式并不复杂,无非就是用一个变量来标记当前是否已经为了某个类创建过对象了,如果创建过对象了,则在下一次获取该类的实例时,直接返回之前创建的对象。
/*单例实现代码如下:*/var Singleton = function(name) { this.name = name; this.instance = null;};Singleton.prototype.getName = function() { alert(this.name);};Singleton.getInstance = function(name) { if (!this.instance) { this.instance = new Singleton(name); } return this.instance; };//测试var xiaoyi = Singleton.getInstance('小一');var xiaoer = Singleton.getInstance('小二');alert( xiaoyi === xiaoer ); //true
/*另一种写法*/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;})();//测试var xiaoyi = Singleton.getInstance('小一');var xiaoer = Singleton.getInstance('小二');alert( xiaoyi === xiaoer ); //true
结论: 无论我们创建的实例叫什么他总是同一个实例,这种写法实现的单例虽然较为简单,但是增加了类的“不透明性”,使用这必须知道这个是一个单例类。
- 透明单例(试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,这个浮窗应该是唯一的,无论单机多少次按钮,每个浮窗只会被创建一次,那么这个浮窗就能用单例模式来创建)现在我们来实现一个“透明”的单例类,这样用户使用这个类就能像使用其他类一样。我们将创建CreateDiv单例类,他的作用是负责在页面创建唯一的div节点。
/*使用透明的单例类来创建div*/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.innnerHTML = this.html; document.body.appendChild(div); }; return CreateDiv;})();//测试var xiaoyi = new CreateDiv('小一');var xiaoer = new CreateDiv('小二');alert( xiaoyi === xiaoer ); //true
结论上边这个单例虽然“透明”了,但是为了把instance封装起来,我们使用了自执行匿名函数和闭包,并且让匿名函数返回真正的Singleton构造方法。增加了程序复杂度。
CreateDiv的构造函数实际上负责了两件事情。第一:创建对象,执行初始话方法,第二:保证只有一个实例对象。违背了单一原则。(单一原则:一个类最好只负责一项功能。控制类的粒度)==导致的结果==当我们要使用这个类实例多个对象的时候,就必须要把控制创建唯一对象的那段去掉,这种修改会给我们带来不必要的烦恼。- javascript中的单例
关于前边所提到的几种单例模式的实现,更多是接近于传统面向对象语言中的实现,单例从类创建而来,在以类为中心的语言中,这是一种很自然的做法。比如java中如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来。
就javascript而言,它其实是一门无类(class-free)语言,在javascript中,既然我们只需要一个“唯一”的对象,为什么要先创建一个类?在javascript中经常把全局变量当成单例来使用。var a = {};
用这种形式创建对象a,a是独一无二的。声明于全局下,提供全局访问点也是必然的。
这种做法缺陷:全局变量存在很多问题,容易造成命名空间污染。自己命名变量随时可能被别人覆盖掉了。【处理】作为开发这我们应该尽量减少全局变量的使用,即使使用也要把它的污染降到最低。- 几种降低全局变量带来的命名污染。
- 使用命名空间
var namespace = {a: {},b: {},};
- 使用闭包封装私有变量
var user = (function(){ var _name = 'sven', _age = 29; return { getUserInfo: function() { return _name + '-' _age; } }})()
- 通用的惰性单例
var getSingle = function(fn) { var result; return function() { return result || (result = fn.apply(this, arguments)); };};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";};var CreteSingleIframe = getSingle(function() { //动态加载第三页面 var iframe = document.createElement('iframe'); document.body.appendChild(iframe); return iframe;});document.getElementById('loginBtn').onclick = function() { var loginLaryer = createSingleIframe(); loginLayer.src = 'http://baidu.com'}
结论 以上我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化而互不影响,而当他们连接在一起,就完成了创建唯一实例对象的功能。
总结当需求实例唯一、命名空间时,就可以使用单例模式。结合闭包特性,用途广泛。
二、结构型设计模式
结构型设计模式 --关注于如何将类或者对象组合成更大的结构,以便在使用的时候更简化。
1. 代理模式
==代理模式(proxy)==:为一个对象提供一个代用品或占位符,以便控制对于它访问。
- 代理模式在生活中的场景
比如:每个明星都有经纪人作为代理。如果想请明星来办一场商业演出,那么只能联系它的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签名确认。
graph LR客户((圆))-->本体((圆))graph LR客户((圆))-->代理((圆))-->本体((圆))
代理相当于本体的替身,替身对请求做出一些处理后再把请求给本体对象。
- 故事场景康康喜欢上玛丽亚,康康在二月十四想送一束花表白玛丽亚,刚好她两有共同的朋友michael,所以康康就叫micheal帮忙送花给玛丽亚。
/*用代码来写送花场景,首先是不通过(代理)*/var Flower = function() {};var kangkang = { //类 sendFlower: function(target) { //传入送花目标 var flower = new Flower(); //实例一朵花 target.receiveFlower(flower); //花传给maria }};var maria = { //类 receiveFlower: function(flower) { //接受类 console.log('收到花了' + flower) }};kangkang.sendFlower(maria);
/*代理送花代码*/var Flower = function() {};var kangkang = { sendFlower: function(target){ var flower = new Flower(); target.receiveFlower(flower); }};var michael = {//michael为代理 receiverFlower: function(flower) { maria.receiveFlower(flower); }};var maria = { receiveFlower: function(flower) { console.log('收到花了' + flower) }};kangkang.sendFlower(michael);
显然看起来这样送花不是有病? 能直接送到手为什么非要转别人的手送花呢。其实不然,当康康自己去送话,如果玛丽亚心情号成功几率有60%,但是心情差成功接近0,而通过jane的话jane可能较为了解玛丽亚,选择心情好的时候去送,这时候就事半功倍了。
/*心情好的时候送花*/var Flower = function() {};var kangkang = { sendFlower: function(target){ var flower = new Flower(); target.receiveFlower(flower); }};var jane = {//jane为代理 receiverFlower: function(flower) { maria.listenGoodMood(function(){ //心情转好是后送花 maria.receiveFlower(flower); }) }};var maria = { receiveFlower: function(flower) { console.log('收到花了' + flower) }, listenGoodMood: function(fn) {//一秒后心情转好 setTimeout(function() { if(true){fn()} fn(); }, 1000); }};kangkang.sendFlower(jane);
- 保护代理和虚拟代理
从上面的例子我们可以看出这两种代理的影子。代理jane可以帮助玛丽亚过滤掉一些请求,比如说送花的人不是康康,这样就直接可以在代理jane中被拒绝掉了。这种代理叫做保护代理。
假设new Flower一束花有期限,过期会凋谢,那么我们可以把new flower 交给代理jane去执行,代理jane会选择在玛丽亚心情好的时候再去买花而后送花。这种代理就叫虚拟代理。(虚拟代理是把一些开销很大的对象,延迟到真正需要它的时候再去创建。)/*虚拟代理*/var jane = {//jane为代理 receiverFlower: function(flower) { maria.listenGoodMood(function(){ //心情转好是后送花 var flower = new Flower(); //延迟创建 flower 对象 maria.receiveFlower(flower); }) }};
- 虚拟代理实现图片的预加载
如果直接给某个img设置src属性的话,可能会由于网络太差,图片的位置往往有段时间会是一片空白,常见的作法就是用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充把img节点中。这种场景就用虚拟代理。
/*创建一个图片元素并且提供一个设置s'r'c的接口*/var myImage = (function() {var imgNode = document.createElement('img');document.body.appendChild(imgNode);return { setSrc: function(src) { imgNode.src = src; }}})();/*引入代理ProxyImg对象,在图片真正被加载好之前用一张展位的菊花图来提示用户正在加载。*/var proxyImage = (function(){ var img = new Image; img.onload = function() { //当真正的图加载完了在把图片塞给他 myImage.setSrc(this.src) } return { setSrc: function(src) { //一开始先设置一张菊花图给他 myImage.setSrc('./loading.gif'); img.src = src; } }})();proxyImage.setSrc('http://wangshangtupian.jpg'); //模拟的图片
- 代理的意义
单一职责原则就一个类而言,应仅有一个引起它变化的原因。
开放-封闭闭原则不用改变MyImage类或者添加接口,通过代理给系统添加了新的行为,这符合开闭原则。结论当我们不需要预加载的功能,我们只需要直接请求本体,而不需要请求代理,这就使用代理的灵活之处。- 虚拟代理合并HTTP请求先想象一下这样一个场景:假设一个公司需要员工写周报,周报要交给总监批阅,总监手下有150个员工,如果我们每个人把周报发给总监,那总监可能就不用工作了,每周看周报。如果将周报发给组长,组长整理后再发给总监,那总监就轻松多了。
/*需要做一个文件同步的功能,当点击checkbox同时往另一台服务器同步文件,如果一秒点四个checkbox,那么网络开销会相当大。我们可以通过一个代理函数来收集一段时间内的请求,最后一次性发给服务器请求*/12345//给checkout 绑定事件var synchronousFile = function(id) { console.log('开始同步文件, id为:' + id);};var ProxySynchronousFile = (function(){ var cache = [],//保存一段事件内需要同步的id timer; //定时器 return function (id){ cache.push(id); if(timer){ //保证不会覆盖已经启动的定时器 return; } timer = setTimeout(function() { synchronousFile(cache.join(',')); //2秒后向服务器发送id的集合 clearTimeout(timer); timer = null; cache.length = 0; //清空ID集合 }, 2000); }})();var checkbox = document.getElementsByTagName('input');for (var i = 0, c; c= checkbox[i++];){ c.onclick = function() { if(this.checked === true){ ProxySynchronousFile(this.id); } }}
- 缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接传出之前的存储的运算结果。
/*计算乘积*/var mult = function() { console.log('开始计算乘积'); var a = 1; for (var i = 0, l = arguments.length; i < l; i++){ a = a * arguments[i]; } return a;}var proxyMult = (function(){ var cache = {}; return function() { var args = Array.prototype.join.call(arguments, ','); if(args in cache){ return cache[args]; } return cache[args] = mult.apply(this, arguments); }})();proxyMult(1, 2, 3, 4); //24proxyMult(1, 2, 3, 4); //24
总结代理模式应用广泛,比如说防火墙代理:控制网络资源的访问,保护主机不让坏人破坏。远程代理:为一个对象在不同的地址空间提供局部代表。保护代理:用于对象应该有不同访问权限情况。智能引用代理:取代了简单的指针,他在访问对象是执行一些附加操作,比如计算一个对象被应用的次数。写时复制代理:通常用于复制一个庞大的对象的情况。写时复制代理延迟了复制的过程。当对象真正被修改时,才对它进行复制操作。写时复制代理时虚拟代理的一种变体。更多就不再一一赘述了。
三、行为型设计模式
行为型设计模式 --不单单涉及到类和对象,更关注于类或者对象之间的通讯交流。
1. 状态模式
==状态模式(State Pattern)==:关键是区分事物内部的状态,事物内部的状态改变往往会带来事物行为的改变。
- 初识状态模式
想象一下:有一个电灯,只有一个控制电灯的开关。当电灯开着的时候按下开关,电灯会切换到关闭状态;再按一次开关,电灯将又被打开,同一个开关在不同的状态下表现出来的行为是不一样的。
var Light = function() { this.state = 'off'; //电灯初始状态 this.button = null; //电灯开关}Light.prototype.init = function() { var button = document.createElement('button'). self = this; button.innerHTML = '开关'; this.button = document.body.appendChild(button); this.button.onclick = function() { self.buttonWasPressed();//开关按下的行为函数 }};Ligth.prototype.buttonWasPressed = function() { if (this.state === 'off') { console.log('开灯'); this.state = 'on'; } else if (this.state === 'on') { console.log('关灯'); this.state = 'off'; }};var light = new Light();light.init();
令人遗憾的是这个世界上的电灯并不是只有这一种。某个酒店的灯,按一下开关弱光,在按一下开关强光,再按一下开光关闭。
Ligth.prototype.buttonWasPressed = function() { if (this.state === 'off') { console.log('弱光'); this.state = 'ruoguang'; } else if (this.state === 'ruoguang') { console.log('强光'); this.state = 'qiangguang'; } else if (this.state === 'qiangguang'){ console.log('关灯'); this.state = 'off'; }};
这个buttonWasPressed很明显违反了开放-闭合原则,每次新增或修改电灯的状态都需要从新改动buttonWasPressed方法中的代码,这让它成为一个非常不稳定的方法。
/*让我们改进一下*/var OffLightState = function(light){ this.light = light;}OffLightState.prototype.buttonWasPressed = function() { console.log('弱光'); this.light.setState(this.light.weakLightState);}var WeakLightState = function(light){ this.light = light;}WeakLightState.prototype.buttonWasPressed = function() { console.log('强光'); this.light.setState(this.light.strongLightState);}var strongLightState = function(light){ this.light = light;}strongLightState.prototype.buttonWasPressed = function() { console.log('关灯'); this.light.setState(this.light.OffLightState);}//改写类var Light = function() { this.OffLightState = new OffLightState(this); this.WeakLightState = new WeakLightState(this); this.strongLightState = new strongLightState(this); this.button = null;};Light.prototype.init = function(){ var button = document.createElement('button'), self = this; this.button = document.body.appendChild(button); this.button.innerHTML = '开关'; this.currState = this.offLightState; //设置当前状态 this.button.onclick = function(){ self.currState.buttonWasPressed(); }}Light.prototype.setState = function(newState){ this.currState = newState;};var light = new Light();light.init();
总结状态模式的优缺点
- 状态模式定义了状态与行为之间的关系,并将他们封装在一个类里,通过增加新的状态,很容易增加新的状态和转换。
- 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了context原本过多的条件分支。
- 用对象代替字符串来记录当前的状态,使得状态的切换更加一目了然。
- context中的请求动作和状态类中封装的行为可以非常容易的独立变化而不互相影响。
- 缺点:会在系统中定义许多的状态类,编写二十个状态类是一项枯燥乏味的工作,而且系统中会因此增加不少对象,另外由于逻辑分布在状态类中,虽然避开了不受欢迎的条件语句,但是也造成了逻辑分散的问题。我们无法在一个地方就看出整个状态机的逻辑。