深入理解javascript之设计模式

来源:互联网 发布:最大公约数算法流程图 编辑:程序博客网 时间:2024/05/16 01:24

设计模式

设计模式是命名、抽象和识别对可重用的面向对象设计有用的的通用设计结构。设计模式确定类和他们的实体、他们的角色和协作、还有他们的责任分配。

每一个设计模式都聚焦于一个面向对象的设计难题或问题。它描述了在其它设计的约束下它能否使用,使用它后的后果和得失。因为我们必须最终实现我们的设计模式,所以每个设计模式都提供了例子,代码来对实现进行阐释.

虽然设计模式被描述为面向对象的设计,它们基于那些已经被主流面向对象语言实现过的解决方案...”。

种类

设计模式可以被分成几个不同的种类。在这个部分我们将分为三类:创建型设计模式、结构设计模式、行为设计模式。

 

创建型设计模式

创建型设计模式关注于对象创建的机制方法,通过该方法,对象以适应工作环境的方式被创建。基本的对象创建方法可能会给项目增加额外的复杂性,而这些模式的目的就是为了通过控制创建过程解决这个问题。

属于这一类的一些模式是:构造器模式(Constructor),工厂模式(Factory),抽象工厂模式(Abstract),原型模式(Prototype),单例模式(Singleton)以及 建造者模式(Builder)。

 

结构设计模式

结构模式关注于对象组成和通常识别的方式实现不同对象之间的关系。该模式有助于在系统的某一部分发生改变的时候,整个系统结构不需要改变。该模式同样有助于对系统中某部分没有达到某一目的的部分进行重组。

在该分类下的模式有:装饰模式,外观模式,享元模式,适配器模式和代理模式。

 

行为设计模式

行为模式关注改善或精简在系统中不同对象间通信。

行为模式包括:迭代模式,中介者模式,观察者模式和访问者模式。

 

 

下面我们通过分开介绍各个常用的设计模式,来加深对设计模式的理解。

构造器模式

构造器是一个当新建对象的内存被分配后,用来初始化该对象的一个特殊函数。对象构造器是被用来创建特殊类型的对象的,首先它要准备使用的对象,其次在对象初次被创建时,通过接收参数,构造器要用来对成员的属性和方法进行赋值。

由于javascript不支持类的概念,所以必须通过构造器来使用new关键字初始化对象。一个基础的构造器代码如下:

function Car( model, year, miles ) {   this.model = model;  this.year = year;  this.miles = miles;   this.toString = function () {    return this.model + " has done " + this.miles + " miles";  };} // 使用: // 我们可以示例化一个Carvar civic = new Car( "Honda Civic", 2009, 20000 );var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); // 打开浏览器控制台查看这些对象toString()方法的输出值// output of the toString() method being called on// these objectsconsole.log( civic.toString() );console.log( mondeo.toString() );

但是这样的话,继承起来比较麻烦,而且每个Car构造函数创建的对象中,toString之类的函数都会被重新定义。所以还是要利用原型,来实现最佳的构造器:

function Car( model, year, miles ) {   this.model = model;  this.year = year;  this.miles = miles; }  // 注意这里我们使用Note here that we are using Object.prototype.newMethod 而不是// Object.prototype ,以避免我们重新定义原型对象Car.prototype.toString = function () {  return this.model + " has done " + this.miles + " miles";}; // 使用: var civic = new Car( "Honda Civic", 2009, 20000 );var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); console.log( civic.toString() );console.log( mondeo.toString() );

工厂模式

一个工厂能提供一个创建对象的公共接口,我们可以在其中指定我们希望被创建的工厂对象的类型。说的简单点,就像饮水机,要咖啡还是牛奶取决于你按哪个按钮。

简单工厂模式在创建ajax对象的时候可以体现出来,可以通过jquery中的$.ajax方法来理解,也可以通过我自己写的一个ajax方法来理解,地址在:http://runjs.cn/code/j5dkikwu


ajax("test002.txt",{type:"GET",data:{name:"liuf",age:23},onsuccess:function(responseText,xhr){document.getElementById("input").value=responseText;},onfail:function(){//document.write("fail");}});

ajax实际上就是一个工厂方法,至于到底是用get方法还是post方法,都由后面的代码来决定。这就是前面所说的“一个工厂能提供一个创建对象的公共接口,我们可以在其中指定我们希望被创建的工厂对象的类型”。

单例模式


单例模式之所以这么叫,是因为它限制一个类只能有一个实例化对象。经典的实现方式是,创建一个类,这个类包含一个方法,这个方法在没有对象存在的情况下,将会创建一个新的实例对象。如果对象存在,这个方法只是返回这个对象的引用。但是javascript本来就是无类的,所以简单地来说,就是没有就创建,有就不创建直接用。

那么我们看看现实中的案例吧,点击一个按钮后出现一个遮罩层,这是一个常用的需求吧。代码如下:

 

var createMask = function(){    return document,body.appendChild(  document.createElement(div)  ); }$( 'button' ).click( function(){    var mask  = createMask();    mask.show(); })

这样写就会出现一个问题,就是每次调用createMask都会创建一个新的div,虽然可以在隐藏遮罩层时将其remove,但是这样还是会带来性能的损耗,那么可以做如下改进,就是在页面一开始就创建div,代码如下:

var mask = document.body.appendChild( document.createElement( ''div' ) ); $( ''button' ).click( function(){    mask.show(); } )

这样确实可以保证页面只会创建一个遮罩层,但是也有一个问题,就是如果用户不需要用到这个div,岂不是白白创建了它。于是我们可以借助一个变量来判断是否已经创建过div,代码如下:

var mask; var createMask = function(){ if ( mask ) return mask; else{ mask = document,body.appendChild(  document.createElement(div)  ); return mask; } }

这样看起来不错,但是mask作为一个全局变量,是否会造成污染呢?所以最好的办法如下:

var createMask = function(){  var mask;  return function(){       return mask || ( mask = document.body.appendChild( document.createElement('div') ) )  }}()

这就是前面所说的“没有就创建,有就不创建直接用”。没错,单例模式就是这么简单,设计模式其实并不难,编程中我们其实一直有用到,只是自己没有发现罢了。

桥接模式

桥接模式就是将实现部分和抽象部分分离开来,以便两者可以独立的变化。在实现api的时候,桥接模式非常常用。

我们以javascript的forEach方法为例:


forEach = function( ary, fn ){  for ( var i = 0, l = ary.length; i < l; i++ ){    var c = ary[ i ];    if ( fn.call( c, i, c ) === false ){      return false;    }   }}

可以看到,forEach函数并不关心fn里面的具体实现. fn里面的逻辑也不会被forEach函数的改写影响.

使用代码如下:

forEach( [1,2,3], function( i, n ){  alert ( n*2 ) } ) forEach( [1,2,3], function( i, n ){   alert ( n*3 ) } )


外观模式

外观模式是一种无处不在的模式,外观模式提供一个高层接口,这个接口使得客户端或者子系统调用起来更加方法。比如:

var getName = function(){  return ''svenzeng"}var getSex = function(){   return 'man'}

现在我要调用两个方法,我就可以使用一个更加高层的接口来调用:

var getUserInfo = function(){  var info = getName () + getSex ();  return info;}

这样就方便组装,如果一开始就把两个写到一个函数中,那就不能够只单独调用其中一个了。


享元模式

享元模式是一个优化重复、缓慢和低效数据共享代码的经典结构化解决方案。它的目标是以相关对象尽可能多的共享数据,来减少应用程序中内存的使用(例如:应用程序的配置、状态等)。通俗的讲,享元模式就是用来减少程序所需的对象个数。

举一个例子,网页中的瀑布流,或者webqq的好友列表中,每次往下拉时,都会创建新的div。那么如果有很对div呢?浏览器岂不是卡死了?所以我们会想到一种办法,就是把已经消失在视线外的div都删除掉,这样页面就可以保持一定数量的节点,但是频繁的删除和添加节点,又会带来很大的性能开销。

这个时候就可以用到享元模式了,享元模式可以提供一些共享的对象以便重复利用。比如页面中只能显示10个div,那始终出现在用户视线中的这10个div就可以写成享元。

原理其实很简单, 把刚隐藏起来的div放到一个数组中, 当需要div的时候, 先从该数组中取, 如果数组中已经没有了, 再重新创建一个. 这个数组里的div就是享元, 它们每一个都可以当作任何用户信息的载体.代码如下:

var getDiv = (function(){    var created = [];    var create = function(){          return document.body.appendChild( document.createElement( 'div' ) );    }    var get = function(){         if ( created.length ){              return created.shift();          }else{                return create();           }     }/* 一个假设的事件,用来监听刚消失在视线外的div,实际上可以通过监听滚动条位置来实现 */      userInfoContainer.disappear(function( div ){              created.push( div );        }) })()  var div = getDiv();  div.innerHTML = "${userinfo}";

适配器模式

适配器模式就是将一个类的接口转换成客户希望的另外一个接口。通俗一点的说,将像苹果手机不能差在电脑机箱上,必须有一个转换器,而这个转换器就是适配器。

在程序里适配器模式也经常用来适配2个接口, 比如你现在正在用一个自定义的js库. 里面有个根据id获取节点的方法$id(). 有天你觉得jquery里的$实现得更酷, 但你又不想让你的工程师去学习新的库和语法. 那一个适配器就能让你完成这件事情.

$id = function( id ){   return jQuery( '#' + id )[0]; }

这样就不用再一个一个的修改了。

代理模式

代理模式就是把对一个对象的访问,交给另一个代理对象来操作。说得通俗一点,程序员每天写日报,日报最后会给总监审阅,但是如果所有人都直接发给总监,那总监就没法工作了。所以每个人会把自己的日报发给自己的组长,再由组长转发给总监。这个组长就是代理。

编程中用到代理模式的情况也不少,比如大量操作dom时,我们会先创建文档碎片,再统一加到dom树中。

示例如下:


中介者模式

中介者模式是观察者模式中的共享被观察者对象。在这个系统中的对象之间直接的发布/订阅关系被牺牲掉了,取而代之的是维护一个通信的中心节点。中介者模式和代理模式是有区别的,区别如下:

中介者对象可以让各个对象之间不需要相互引用,从而使其耦合松散,而且可以独立的改变透明之间的交互。通俗点讲,银行在存款人和贷款人之间也能看成一个中介。存款人A并不关心他的钱最后被谁借走。贷款人B也不关心他借来的钱来自谁的存款。因为有中介的存在,这场交易才变得如此方便。

在编程中,大名鼎鼎的MVC结构中的Controller不就是一个中介者吗?拿backbone举例. 一个mode里的数据并不确定最后被哪些view使用. view需要的数据也可以来自任意一个mode. 所有的绑定关系都是在controler里决定. 中介者把复杂的多对多关系, 变成了2个相对简单的1对多关系。

 

示例代码如下:

var mode1 = Mode.create(),  mode2 = Mode.create();var view1 = View.create(),   view2 = View.create();var controler1 = Controler.create( mode1, view1, function(){  view1.el.find( ''div' ).bind( ''click', function(){    this.innerHTML = mode1.find( 'data' );  } )})var controler2 = Controler.create( mode2 view2, function(){  view1.el.find( ''div' ).bind( ''click', function(){    this.innerHTML = mode2.find( 'data' );  } )})

观察者模式

 

观察者模式是这样一种设计模式。一个被称作被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。

当一个被观察者需要将一些变化通知给观察者的时候,它将采用广播的方式,这条广播可能包含特定于这条通知的一些数据。

当特定的观察者不再需要接受来自于它所注册的被观察者的通知的时候,被观察者可以将其从所维护的组中删除。

通俗点理解,就是面试官是被观察者,而等待通知的人是观察者。

javascript中平时接触的dom事件,其实就是一种观察者模式的体现:


div.onclick  =  function click (){    alert ( ''click' ) }

只要订阅了div的click事件,当点击div的时候,click函数就会执行。

观察者模式可以很好的实现连个模块之间的解耦,假如一个多人合作的项目,我负责Map和Gamer模式:


loadImage(  imgAry,  function(){ Map.init(); Gamer.init(); } )
<p>而别人负责loadImage方法的维护,如果有一天,我要加上一个Sound模块,而我无权动用别人的代码,只能空空等待别人回来添加:</p><pre name="code" class="html">loadImage(  imgAry,  function(){ Map.init(); Gamer.init(); Sount.init(); } )

所以我们可以做如下改动:

loadImage.listen( ''ready', function(){     Map.init(); }) loadImage.listen( ''ready', function(){    Gamer.init(); }) loadImage.listen( ''ready', function(){    Sount.init(); })

loadImage完成之后,不用再关心未来会发送什么,接下来它只需要发送一个信号:


loadImage.trigger( ‘ready’ );


所有监听ready事件的对象就都会收到通知。这就是观察者模式的应用场景。


说到这里,常用的设计模式也讲完了,深入理解javascript系列也将告一段落。







3 0
原创粉丝点击