找回密码
 立即注册
首页 业界区 业界 javascript 设计模式 - 文章很长,请自备瓜子,水果和眼 ...

javascript 设计模式 - 文章很长,请自备瓜子,水果和眼药水

巨耗 2025-5-29 15:55:17
  一直都在考虑这个月分享大家什么东西最好,原计划是打算写一些HTML5中JS方面的内容或者是AJAX方面的,可是自己由于表达能力,时间,还有个人工作方面的问题,这个还是等下个月再做分享吧^.^。
  老规矩,开始正文以前先交代自己写这篇文章的目的和一些注意事项:
  1.首先本人一直从事前端开发,所以除了JavaScript其他的语言了解不深,所以文章只会以JavaScript语言的角度去论证;
  2.其实我个人在项目用过的模式也不多,对模式的概念的理解也没有那么抽象,所以最近在面试中如果面试官问到与模式相关的问题,自己感觉在对答过程中很郁闷,很多东西表达不清楚,于是就找了些相关资料,才会有这篇文章分享;
  3.JavaScript模式与前端的工作和成长密不可分,因为这确实不是一个简单的话题,所以我只能尽力用简单表达和例子阐明,而且园子里有很多的高手,所以希望大家踊跃发言(由于水平有限,请大家多多指教,希望嘴下留情);
  4.由于这篇文章更多的只是想起到一个介绍和讲解的作用,并不打算对每种模式进行细致的分析,所以每种模式只用到一个至二个例子,可能会造成这个例子的表达并不是最优的或者不够全面,如果各位看官觉得不过瘾,可以再去查找相关资料;
  5.做任何事都需要坚持,写博客也是一样,嘿嘿,每月至少一篇(文章确实较长,希望能对朋友们有所帮助,重点部分在前言中有介绍,大家可以选择感兴趣的模式进行深入)。
  6.欢迎转载,不过请注明出处,谢谢。
 
了解JavaScript设计模式我们需要知道的一些必要知识点:(内容相对基础,高手请跳过)
  闭包:关于闭包这个月在园子里有几篇不错的分享了,在这我也从最实际的地方出发,说说我的理解。
    1.闭包最常用的方式就是返回一个内联函数(何为内联函数?就是在函数内部声明的函数);
    2.在JavaScript中有作用域和执行环境的问题,在函数内部的变量在函数外部是无法访问的,在函数内部却可以得到全局变量。由于种种原因,我们有时候需要得到函数内部的变量,可是用常规方法是得不到的,这时我们就可以创建一个闭包,用来在外部访问这个变量。
    3.闭包的用途 主要就是上一点提到的读取函数内部变量,还有一个作用就是可以使这些变量一直保存在内存中。
    4.使用闭包要注意,由于变量被保存在内存中,所以会对内存造成消耗,所以不能滥用闭包。解决方法是 在退出函数之前,将不使用的局部变量全部删除。
    最后还是上一套闭包的代码吧,这样更直观。
  1. 1   function f(){<br> 2     var n = 999;<br> 3     function f1(){<br> 4       alert(n+=1);<br> 5     }<br> 6     return f1;<br> 7   }<br> 8   var result = f();<br> 9   result(); // 1000<br>10   result(); // 1001<br>11   result(); // 1002
复制代码
  封装:通过将一个方法或者属性声明为私用的,可以让对象的实现细节对其他对象保密以降低对象之间的耦合程度,可以保持数据的完整性并对其修改方式加以约束,这样可以是代码更可靠,更易于调试。封装是面向对象的设计的基石。
  尽管JavaScript是一门面向对象的语言,可它并不具备将成员声明为公用或私用的任何内部机制,所以我们只能自己想办法实现这种特性。下面还是通过一套完整的代码去分析,介绍什么是私有属性和方法,什么是特权属性和方法,什么是属性和方法,什么是静态属性和方法。
  私有属性和方法:函数有作用域,在函数内用var 关键字声明的变量在外部无法访问,私有属性和方法本质就是你希望在对象外部无法访问的变量。
  特权属性和方法:创建属性和方法时使用的this关键字,因为这些方法定义在构造器的作用域中,所以它们可以访问到私有属性和方法;只有那些需要直接访问私有成员的方法才应该被设计为特权方法。
  共有属性和方法:直接链在prototype上的属性和方法,不可以访问构造器内的私有成员,可以访问特权成员,子类会继承所有的共有方法。
  共有静态属性和方法:最好的理解方式就是把它想象成一个命名空间,实际上相当于把构造器作为命名空间来使用。
 
  1. 1   /* -- 封装 -- */<br> 2   var _packaging =function(){<br> 3      //私有属性和方法<br> 4      var name ='Darren';<br> 5      var method1 =function(){<br> 6         //...<br> 7      }<br> 8      //特权属性和方法<br> 9      this.title ='JavaScript Design Patterns' ;<br>10      this.getName =function(){<br>11         return name;<br>12      }<br>13   }<br>14   //共有静态属性和方法<br>15   _packaging._name ='Darren code';<br>16   _packaging.alertName =function(){<br>17      alert(_packaging._name);<br>18   }<br>19   //共有属性和方法<br>20   _packaging.prototype = {<br>21      init:function(){<br>22         //...<br>23      }<br>24   }
复制代码
  继承:继承本身就是一个抽象的话题,在JavaScript中继承更是一个复杂的话题,因为JavaScript想要实现继承有两种实现方式,分别是类式继承和原型式继承,每种实现的方式都需要采取不少措施,下面本人通过分析例子的方式讲解JavaScript中这个很重要的话题。
  1. 1   /* -- 类式继承 -- */<br> 2   //先声明一个超类<br> 3   function Person(name){<br> 4     this.name = name;<br> 5   }<br> 6   //给这个超类的原型对象上添加方法 getName <br> 7   Person.prototype.getName =function(){<br> 8      returnthis.name;<br> 9   }<br>10   //实例化这个超类<br>11   var a =new Person('Darren1')<br>12   alert(a.getName());<br>13   //再声明类<br>14   function Programmer(name,sex){<br>15      //这个类中要调用超类Person的构造函数,并将参数name传给它<br>16      Person.call(this,name);<br>17      this.sex = sex;<br>18   }<br>19   //这个子类的原型对象等于超类的实例<br>20   Programmer.prototype =new Person();<br>21   //因为子类的原型对象等于超类的实例,所以prototype.constructor这个方法也等于超类构造函数,你可以自己测试一下,如果没这一步,alert(Programmer.prototype.constructor),这个是Person超类的引用,所以要从新赋值为自己本身<br>22   Programmer.prototype.constructor = Programmer;<br>23   //子类本身添加了getSex 方法<br>24   Programmer.prototype.getSex =function(){<br>25      returnthis.sex;<br>26   }<br>27   //实例化这个子类<br>28   var _m =new Programmer('Darren2','male');<br>29   //自身的方法<br>30   alert(_m.getSex());<br>31   //继承超类的方法<br>32   alert(_m.getName());
复制代码
  代码都不难,只要对 原型链 有基础就能理解。类式继承模式是JavaScript继承主要的模式,几乎所有用面向对象方式编写的JavaScript代码中都用到了这种继承,又因为在各种流行语言中只有JavaScript使用原型式继承,因此最好还是使用类式继承。可是要熟悉JavaScript语言,原型继承也是我们必须所了解的,至于在项目中是否使用就得看个人编码风格了。
  1. 1   /* -- 原型式继承 -- */<br> 2   //clone()函数用来创建新的类Person对象<br> 3   var clone =function(obj){<br> 4 var _f =function(){};<br> 5      <strong>//这句是原型式继承最核心的地方,函数的原型对象为对象字面量</strong><br> 6      _f.prototype = obj; <br> 7      returnnew _f;<br> 8   }<br> 9   //先声明一个对象字面量<br>10   var Person = {<br>11      name:'Darren',<br>12      getName:function(){<br>13         returnthis.name;<br>14      }<br>15   }<br>16   //不需要定义一个Person的子类,只要执行一次克隆即可<br>17   var Programmer = clone(Person);<br>18   //可以直接获得Person提供的默认值,也可以添加或者修改属性和方法<br>19   alert(Programmer.getName())<br>20   Programmer.name ='Darren2'<br>21   alert(Programmer.getName())<br>22 <br>23   //声明子类,执行一次克隆即可<br>24   var Someone = clone(Programmer);
复制代码
  ------------------------------------------  正文开始了,我是分割线  ------------------------------------------
 
  前言:
  JavaScript设计模式的作用 - 提高代码的重用性,可读性,使代码更容易的维护和扩展。
   
  1.单体模式,工厂模式,桥梁模式个人认为这个一个优秀前端必须掌握的模式,对抽象编程和接口编程都非常有好处。
  2.装饰者模式和组合模式有很多相似的地方,它们都与所包装的对象实现同样的接口并且会把任何方法的调用传递给这些对象。装饰者模式和组合模式是本人描述的较吃力的两个模式,我个人其实也没用过,所以查了很多相关资料和文档,请大家海涵。
  3.门面模式是个非常有意思的模式,几乎所有的JavaScript库都会用到这个模式,假如你有逆向思维或者逆向编程的经验,你会更容易理解这个模式(听起来有挑战,其实一接触你就知道这是个很简单的模式);还有配置器模式得和门面模式一块拿来说,这个模式对现有接口进行包装,合理运用可以很多程度上提高开发效率。这两个模式有相似的地方,所以一块理解的话相信都会很快上手的。
  4.享元模式是一种以优化为目的的模式。
  5.代理模式主要用于控制对象的访问,包括推迟对其创建需要耗用大量计算资源的类得实例化。
  6.观察者模式用于对对象的状态进行观察,并且当它发生变化时能得到通知的方法。用于让对象对事件进行监听以便对其作出响应。观察者模式也被称为“订阅者模式”。
  7.命令模式是对方法调用进行封装的方式,用命名模式可以对方法调用进行参数化和传递,然后在需要的时候再加以执行。
  8.职责链模式用来消除请求的发送者和接收者之间的耦合。
 
 
  JavaScript设计模式都有哪些?
  单体(Singleton)模式: 绝对是JavaScript中最基本最有用的模式。
  单体在JavaScript的有多种用途,它用来划分命名空间。可以减少网页中全局变量的数量(在网页中使用全局变量有风险);可以在多人开发时避免代码的冲突(使用合理的命名空间)等等。
  在中小型项目或者功能中,单体可以用作命名空间把自己的代码组织在一个全局变量名下;在稍大或者复杂的功能中,单体可以用来把相关代码组织在一起以便日后好维护。  
  使用单体的方法就是用一个命名空间包含自己的所有代码的全局对象,示例:
  1. 1   var functionGroup = {<br>2     name:'Darren',<br>3     method1:function(){<br>4       //code<br>5     },<br>6     init:function(){<br>7       //code<br>8     }<br>9   }
复制代码
  或者
  1. 1   var functionGroup  =newfunction myGroup(){<br>2     this.name ='Darren';<br>3     this.getName =function(){<br>4       returnthis.name<br>5     }<br>6     this.method1 =function(){}<br>7     ...<br>8   }
复制代码
   
  工厂(Factory)模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类。
  工厂就是把成员对象的创建工作转交给一个外部对象,好处在于消除对象之间的耦合(何为耦合?就是相互影响)。通过使用工厂方法而不是new关键字及具体类,可以把所有实例化的代码都集中在一个位置,有助于创建模块化的代码,这才是工厂模式的目的和优势。
  举个例子:你有一个大的功能要做,其中有一部分是要考虑扩展性的,那么这部分代码就可以考虑抽象出来,当做一个全新的对象做处理。好处就是将来扩展的时候容易维护 - 只需要操作这个对象内部方法和属性,达到了动态实现的目的。非常有名的一个示例 - XHR工厂
  1. 1   var XMLHttpFactory =function(){};      //这是一个简单工厂模式<br> 2   XMLHttpFactory.createXMLHttp =function(){<br> 3     var XMLHttp = null;<br> 4     if (window.XMLHttpRequest){<br> 5       XMLHttp = new XMLHttpRequest()<br> 6     }elseif (window.ActiveXObject){<br> 7       XMLHttp = new ActiveXObject("Microsoft.XMLHTTP")<br> 8     }<br>10   return XMLHttp;<br>11   }<br>12   //XMLHttpFactory.createXMLHttp()这个方法根据当前环境的具体情况返回一个XHR对象。<br>13   var AjaxHander =function(){<br>14     var XMLHttp = XMLHttpFactory.createXMLHttp();<br>15     ...<br>16   }
复制代码
  工厂模式又区分简单工厂模式和抽象工厂模式,上面介绍的是简单工厂模式,这种模式用的更多也更简单易用。抽象工厂模式的使用方法就是 - 先设计一个抽象类,这个类不能被实例化,只能用来派生子类,最后通过对子类的扩展实现工厂方法。 示例:

  1. 1   var XMLHttpFactory =function(){};      //这是一个抽象工厂模式<br> 2   XMLHttpFactory.prototype = {<br> 3      //如果真的要调用这个方法会抛出一个错误,它不能被实例化,只能用来派生子类<br> 4      createFactory:function(){<br> 5         thrownew Error('This is an abstract class');<br> 6      }<br> 7   }<br> 8   //派生子类,文章开始处有基础介绍那有讲解继承的模式,不明白可以去参考原理<br> 9   var XHRHandler =function(){<br>10      XMLHttpFactory.call(this);<br>11   };<br>12   XHRHandler.prototype =new XMLHttpFactory();<br>13   XHRHandler.prototype.constructor = XHRHandler;<br>14   //重新定义createFactory 方法<br>15   XHRHandler.prototype.createFactory =function(){<br>16      var XMLHttp =null;<br>17      if (window.XMLHttpRequest){<br>18         XMLHttp =new XMLHttpRequest()<br>19      }elseif (window.ActiveXObject){<br>20         XMLHttp =new ActiveXObject("Microsoft.XMLHTTP")<br>21      }<br>22      return XMLHttp;<br>23   }
复制代码
  
  桥接(bridge)模式:在实现API的时候,桥梁模式灰常有用。在所有模式中,这种模式最容易立即付诸实施。
  桥梁模式可以用来弱化它与使用它的类和对象之间的耦合,就是将抽象与其实现隔离开来,以便二者独立变化;这种模式对于JavaScript中常见的时间驱动的编程有很大益处,桥梁模式最常见和实际的应用场合之一是时间监听器回调函数。先分析一个不好的示例:
 
  1. 1   element.onclick =function(){<br>2      new setLogFunc();<br>3   };
复制代码
 
  为什么说这个示例不好,因为从这段代码中无法看出那个LogFunc方法要显示在什么地方,它有什么可配置的选项以及应该怎么去修改它。换一种说法就是,桥梁模式的要诀就是让接口“可桥梁”,实际上也就是可配置。把页面中一个个功能都想象成模块,接口可以使得模块之间的耦合降低。
  掌握桥梁模式的正确使用收益的不只是你,还有那些负责维护你代码的人。把抽象于其实现隔离开,可独立地管理软件的各个部分,bug也因此更容易查找。
  桥梁模式目的就是让API更加健壮,提高组件的模块化程度,促成更简洁的实现,并提高抽象的灵活性。一个好的示例:
  1. 1   element.onclick =function(){  //API可控制性提高了,使得这个API更加健壮<br>2      new someFunction(element,param,callback);<br>3   }
复制代码
  注:桥梁模式还可以用于连接公开的API代码和私有的实现代码,还可以把多个类连接在一起。在文章封装介绍的部分提到过特权方法,也是桥梁模式的一种特例。《JS设计模式》上找的示例,加深大家对这个模式的理解:
  1. 1   //错误的方式<br> 2   //这个API根据事件监听器回调函数的工作机制,事件对象被作为参数传递给这个函数。本例中并没有使用这个参数,而只是从this对象获取ID。<br> 3   addEvent(element,'click',getBeerById);<br> 4   function(e){<br> 5      var id =this.id;<br> 6      asyncRequest('GET','beer.url?id='+ id,function(resp){<br> 7         //Callback response<br> 8         console.log('Requested Beer: '+ resp.responseText);<br> 9      });<br>10   }<br>11 <br>12   //好的方式<br>13   //从逻辑上分析,把id传给getBeerById函数式合情理的,且回应结果总是通过一个毁掉函数返回。这么理解,我们现在做的是针对接口而不是实现进行编程,用桥梁模式把抽象隔离开来。<br>14   function getBeerById(id,callback){<br>15      asyncRequest('GET','beer.url?id='+ id,function(resp){<br>16         callback(resp.responseText)<br>17      });<br>18   }<br>19   addEvent(element,'click',getBeerByIdBridge);<br>20   function getBeerByIdBridge(e){<br>21      getBeerById(this.id,function(beer){<br>22         console.log('Requested Beer: '+ beer);<br>23      });<br>24   } 
复制代码
 
  装饰者(Decorator)模式:这个模式就是为对象增加功能(或方法)。
  动态地给一个对象添加一些额外的职责。就扩展功能而言,它比生成子类方式更为灵活。
  装饰者模式和组合模式有很多共同点,它们都与所包装的对象实现统一的接口并且会把任何方法条用传递给这些对象。可是组合模式用于把众多子对象组织为一个整体,而装饰者模式用于在不修改现有对象或从派生子类的前提下为其添加方法。
  装饰者的运作过程是透明的,这就是说你可以用它包装其他对象,然后继续按之前使用那么对象的方法来使用,从下面的例子中就可以看出。还是从代码中理解吧:
  1. 1   //创建一个命名空间为myText.Decorations<br> 2   var myText= {};<br> 3   myText.Decorations={};<br> 4   myText.Core=function(myString){<br> 5      this.show =function(){return myString;}<br> 6   }<br> 7   //第一次装饰<br> 8   myText.Decorations.addQuestuibMark =function(myString){<br> 9      this.show =function(){return myString.show()+'?';};<br>10   }<br>11   //第二次装饰<br>12   myText.Decorations.makeItalic =function(myString){<br>13      this.show =function(){return'<li>'+myString.show()+'</li>'};<br>14   }<br>15   //得到myText.Core的实例<br>16   var theString =new myText.Core('this is a sample test String');<br>17   alert(theString.show());  //output 'this is a sample test String'<br>18   theString =new myText.Decorations.addQuestuibMark(theString);<br>19   alert(theString.show());  //output 'this is a sample test String?'<br>20   theString =new myText.Decorations.makeItalic (theString);<br>21   alert(theString.show());  //output '<li>this is a sample test String</li>'
复制代码
 
  从这个示例中可以看出,这一切都可以不用事先知道组件对象的接口,甚至可以动态的实现,在为现有对象增添特性这方面,装饰者模式有极大的灵活性。
  如果需要为类增加特性或者方法,而从该类派生子类的解决办法并不实际的话,就应该使用装饰者模式。派生子类之所以会不实际最常见的原因是需要添加的特性或方法的数量要求使用大量子类。
 
 
  组合(Composite)模式:将对象组合成树形结构以表示“部分-整体”的层次结构。它使得客户对单个对象和复合对象的使用具有一致性。
  组合模式是一种专为创建Web上的动态用户界面而量身定制的模式。使用这种模式,可以用一条命令在多个对象上激发复杂的或递归的行为。组合模式擅长于对大批对象进行操作。
  组合模式的好处:1.程序员可以用同样的方法处理对象的集合与其中的特定子对象;2.它可以用来把一批子对象组织成树形结构,并且使整棵树都可被便利。
  组合模式适用范围:1.存在一批组织成某处层次体系的对象(具体结构可能在开发期间无法知道);2.希望对这批对象或其中的一部分对象实话一个操作。
  其实组合模式就是将一系列相似或相近的对象组合在一个大的对象,由这个大对象提供一些常用的接口来对这些小对象进行操作,代码可重用,对外操作简单。例如:对form内的元素,不考虑页面设计的情况下,一般就剩下input了,对于这些input都有name和value的属性,因此可以将这些input元素作为form对象的成员组合起来,form对象提供对外的接口,便可以实现一些简单的操作,比如设置某个input的value,添加/删除某个input等等。
  这种模式描述起来比较吃力,我从《JS设计模式》上找个一个实例,大家还是看代码吧:先创建组合对象类
  1. 1   // DynamicGallery Class<br> 2   var DynamicGallery =function (id) { // 实现Composite,GalleryItem组合对象类 <br> 3      this.children = [];<br> 4      this.element = document.createElement('div');<br> 5      this.element.id = id;<br> 6      this.element.className ='dynamic-gallery';<br> 7   }<br> 8   DynamicGallery.prototype = {<br> 9      // 实现Composite组合对象接口 <br>10      add: function (child) {<br>11         this.children.push(child);<br>12         this.element.appendChild(child.getElement());<br>13      },<br>14      remove: function (child) {<br>15         for (var node, i =0; node =this.getChild(i); i++) {<br>16            if (node == child) {<br>17               this.children.splice(i, 1);<br>18               break;<br>19            }<br>20         }<br>21         this.element.removeChild(child.getElement());<br>22      },<br>23      getChild: function (i) {<br>24         returnthis.children[i];<br>25      },<br>26      // 实现DynamicGallery组合对象接口 <br>27      hide: function () {<br>28         for (var node, i =0; node =this.getChild(i); i++) {<br>29            node.hide();<br>30         }<br>31         this.element.style.display ='none';<br>32      },<br>33      show: function () {<br>34         this.element.style.display ='block';<br>35         for (var node, i =0; node = getChild(i); i++) {<br>36            node.show();<br>37         }<br>38      },<br>39      // 帮助方法 <br>40      getElement: function () {<br>41         returnthis.element;<br>42      }<br>43   }
复制代码
  再创建叶对象类
  1. 1   var GalleryImage =function (src) { // 实现Composite和GalleryItem组合对象中所定义的方法 <br> 2      this.element = document.createElement('img');<br> 3      this.element.className ='gallery-image';<br> 4      this.element.src = src;<br> 5   }<br> 6   GalleryImage.prototype = {<br> 7      // 实现Composite接口 <br> 8      // 这些是叶结点,所以我们不用实现这些方法,我们只需要定义即可 <br> 9      add: function () { },<br>10      remove: function () { },<br>11      getChild: function () { },<br>12      // 实现GalleryItem接口 <br>13      hide: function () {<br>14         this.element.style.display ='none';<br>15      },<br>16      show: function () {<br>17         this.element.style.display ='';<br>18      },<br>19      // 帮助方法 <br>20      getElement: function () {<br>21         returnthis.element;<br>22      }<br>23   }
复制代码
  现在我们可以使用这两个类来管理图片:
[code] 1   var topGallery =new DynamicGallery('top-gallery');
2   topGallery.add(new GalleryImage('/img/image-1.jpg'));
3   topGallery.add(new GalleryImage('/img/image-2.jpg'));
4   topGallery.add(new GalleryImage('/img/image-3.jpg'));
5   var vacationPhotos =new DyamicGallery('vacation-photos');
6   for(var i =0, i
您需要登录后才可以回帖 登录 | 立即注册