Javascript设计模式
使用Javascript有很长一段时间了,现在觉得自己有了一定的瓶颈,读jQuery源码吃力,尝试着把自己写的面向过程的特效封装成插件也封装不好,学习MVC设计模式的框架也总是弄不懂,思来想去,还是觉得要静下心来学学设计模式以及设计模式涉及到的一些相关的基础,否则难以提高。以前读大学的时候学Java,看到24种设计模式简直惊呆了,那时候好不容易从Java各种类库中挣脱出来,以为可以开始干点事情了,结果碰上24种设计模式和当时很流行的SSH三大框架,我就直接放弃了。该面对的坎始终还是要面对的,在JS和JS的主流库们里面挣扎了那么久,到头来还是要看看设计模式的东西,还是有一堆前端框架在等着我,突然想起一句话,“为了生活每日都要洗身躯!”
不打算看犀牛书了,我觉得我越来越讨厌实体书了,我甚至都很讨厌PDF了,我觉得博客园上的大牛和示例就足够好了,讲一大堆概念不如上一段代码,show me your code!所以我的这篇笔记会以示例为主。
准备工作
闭包
个人认为闭包是一个很抽象的概念,简单来说就是作用域,问题是作用域谁不懂啊,花括号对的开始和结尾嘛!
而且这个问题经常会在面试中提到,很多面试官问你懂不懂闭包,试着举一个闭包的例子,其实是为了考察你对面向对象和设计模式的理解,如果对这些都不理解,谈何大型框架?又哪来的效率开发呢?想必你又是一个重复造轮子的傻缺!
唉!我想到我以前面试时的弱智回答,真是细思恐极!话不多说看代码!
function f(){ var n = 999; function f1(){ alert(n+=1); } return f1; } var result = f(); result(); // 1000 result(); // 1001 result(); // 1002
看完以后发现,噢,原来说复杂一点,也就是可以在函数内部构建内联函数嘛!让外面的这个f()有一种,“咦!这个函数看上去像个class有木有”的感觉。
封装
私有属性和方法:函数有作用域,在函数内用var 关键字声明的变量在外部无法访问,私有属性和方法本质就是你希望在对象外部无法访问的变量。
特权属性和方法:创建属性和方法时使用的this关键字,因为这些方法定义在构造器的作用域中,所以它们可以访问到私有属性和方法;只有那些需要直接访问私有成员的方法才应该被设计为特权方法。
公有属性和方法:直接链在prototype上的属性和方法,不可以访问构造器内的私有成员,可以访问特权成员,子类会继承所有的共有方法。
公有静态属性和方法:最好的理解方式就是把它想象成一个命名空间,实际上相当于把构造器作为命名空间来使用。
var _packaging =function(){ //私有属性和方法 var name ='Darren'; var method1 =function(){ //... } //特权属性和方法 this.title ='JavaScript Design Patterns' ; this.getName =function(){ return name; } } //共有静态属性和方法 _packaging._name ='Darren code'; _packaging.alertName =function(){ alert(_packaging._name); } //共有属性和方法 _packaging.prototype = { init:function(){ //... } }
继承
继承本身就是一个抽象的话题,在JavaScript中继承更是一个复杂的话题,因为JavaScript想要实现继承有两种实现方式,分别是类式继承和原型式继承,每种实现的方式都需要采取不少措施。
类式继承
//先声明一个超类 function Person(name){ this.name = name; } //给这个超类的原型对象上添加方法 getName Person.prototype.getName =function(){ return this.name; } //实例化这个超类 var a =new Person('Darren1') alert(a.getName()); //再声明类 function Programmer(name,sex){ //这个类中要调用超类Person的构造函数,并将参数name传给它 Person.call(this,name); this.sex = sex; } //这个子类的原型对象等于超类的实例 Programmer.prototype =new Person(); //因为子类的原型对象等于超类的实例,所以prototype.constructor这个方法也等于超类构造函数, //如果没这一步,alert(Programmer.prototype.constructor),这个是Person超类的引用,所以要从新赋值为自己本身 Programmer.prototype.constructor = Programmer; //子类本身添加了getSex 方法 Programmer.prototype.getSex =function(){ return this.sex; } //实例化这个子类 var _m =new Programmer('Darren2','male'); //自身的方法 alert(_m.getSex()); //继承超类的方法 alert(_m.getName());
类式继承模式是JavaScript继承主要的模式,几乎所有用面向对象方式编写的JavaScript代码中都用到了这种继承,又因为在各种流行语言中只有JavaScript使用原型式继承,因此最好还是使用类式继承。可是要熟悉JavaScript语言,原型继承也是我们必须所了解的,至于在项目中是否使用就得看个人编码风格了。
原型式继承
//clone()函数用来创建新的类Person对象 var clone =function(obj){ var _f =function(){}; //这句是原型式继承最核心的地方,函数的原型对象为对象字面量 _f.prototype = obj; return new _f; } //先声明一个对象字面量 var Person = { name:'Darren', getName:function(){ return this.name; } } //不需要定义一个Person的子类,只要执行一次克隆即可 var Programmer = clone(Person); //可以直接获得Person提供的默认值,也可以添加或者修改属性和方法 alert(Programmer.getName()) Programmer.name ='Darren2' alert(Programmer.getName()) //声明子类,执行一次克隆即可 var Someone = clone(Programmer);
设计模式
单体模式/单例模式(Singleton)
描述:单体模式是用来划分命名空间,减少网页中全局变量的数量的。
作用:在项目中可以避免多人开发产生的代码冲突。
var functionGroup =newfunction myGroup(){ this.name ='Darren'; this.getName =function(){ returnthis.name } this.method1 =function(){} ... }
工厂模式(Factory)
描述:你有一个大的功能要做,其中有一部分是要考虑扩展性的,那么这部分代码就可以考虑抽象出来,当做一个全新的对象做处理。
作用:将来扩展的时候容易维护,只需要操作这个对象内部方法和属性,达到了动态实现的目的。
分为简单工厂模式和抽象工厂模式
简单工厂模式
var XMLHttpFactory =function(){}; XMLHttpFactory.createXMLHttp =function(){ var XMLHttp = null; if (window.XMLHttpRequest){ XMLHttp = new XMLHttpRequest() }else if (window.ActiveXObject){ XMLHttp = new ActiveXObject("Microsoft.XMLHTTP") } return XMLHttp; } //XMLHttpFactory.createXMLHttp()这个方法根据当前环境的具体情况返回一个XHR对象。 var AjaxHander =function(){ var XMLHttp = XMLHttpFactory.createXMLHttp(); ... }
抽象工厂模式:先设计一个抽象类,这个类不能被实例化,只能用来派生子类,最后通过对子类的扩展实现工厂方法。
var XMLHttpFactory=function(){}; XMLHttpFactory.prototype={ //如果真的要调用这个方法会抛出一个错误,它不能被实例化,只能用来派生子类 createFactory:function(){ thrownewError('Thisisanabstractclass'); } } //派生子类 var XHRHandler=function(){ XMLHttpFactory.call(this); }; XHRHandler.prototype=new XMLHttpFactory(); XHRHandler.prototype.constructor=XHRHandler; //重新定义createFactory方法 XHRHandler.prototype.createFactory=function(){ var XMLHttp=null; if(window.XMLHttpRequest){ XMLHttp=new XMLHttpRequest() }else if(window.ActiveXObject){ XMLHttp=new ActiveXObject("Microsoft.XMLHTTP") } return XMLHttp; }
桥接模式/桥梁模式(Bridge)
描述:将问题的抽象和现实分离开来实现。
作用:抽象和现实两者可以独立的变化。在实现API的时候,桥接模式特别有用,它可以让API更健壮,提高组件的模块化程度。
比如我们常用的forEach()函数的实现
var 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函数的改写影响。
装饰者模式(Decorator)
组合模式(Composite)
门面模式(Facade)
适配器模式/配置器模式(Adapter)
享元模式(FlyWeight)
代理模式(Proxy)
观察者模式/发布者-订阅者模式(Observer)
最常用的模式之一,在很多语言里都得到大量应用,包括我们平时接触的dom事件,也是js和dom之间实现的一种观察者模式。
div.onclick = function click (){ alert ('click'); }
只要订阅了div的click事件,当点击div的时候,function click就会被触发。
那么到底什么是观察者模式呢?先看看生活中的观察者模式。
好莱坞有句名言,“不要给我打电话, 我会给你打电话” 这句话就解释了一个观察者模式的来龙去脉。 其中“我”是发布者, “你”是订阅者。
再举个例子,我来公司面试的时候,完事之后每个面试官都会对我说:“请留下你的联系方式, 有消息我们会通知你”。 在这里“我”是订阅者, 面试官是发布者。所以我不用每天或者每小时都去询问面试结果, 通讯的主动权掌握在了面试官手上。而我只需要提供一个联系方式。
观察者模式可以很好的实现2个模块之间的解耦。 假如我正在一个团队里开发一个html5游戏. 当游戏开始的时候,需要加载一些图片素材。加载好这些图片之后开始才执行游戏逻辑. 假设这是一个需要多人合作的项目. 我完成了Gamer和Map模块, 而我的同事A写了一个图片加载器loadImage.
loadImage(imgAry, function(){ Map.init(); Gamer.init(); });
当图片加载好之后, 再渲染地图, 执行游戏逻辑. 嗯, 这个程序运行良好. 突然有一天, 我想起应该给游戏加上声音功能. 我应该让图片加载器添上一行代码.
loadImage(imgAry, function(){ Map.init(); Gamer.init(); Sound.init(); });
可是写这个模块的同事A去了外地旅游. 于是我打电话给他, 喂. 你的loadImage函数在哪, 我能不能改一下, 改了之后有没有副作用. 如你所想, 各种不淡定的事发生了. 如果当初我们能这样写呢?
loadImage.listen('ready', function(){ Map.init(); }); loadImage.listen('ready', function(){ Gamer.init(); }); loadImage.listen('ready', function(){ Sount.init(); });
loadImage完成之后, 它根本不关心将来会发生什么, 因为它的工作已经完成了. 接下来它只要发布一个信号
loadImage.trigger('ready’);
那么监听了loadImage的’ready’事件的对象都会收到通知. 就像上个面试的例子. 面试官根本不关心面试者们收到面试结果后会去哪吃饭. 他只负责把面试者的简历搜集到一起. 当面试结果出来时照着简历上的电话挨个通知.
说了这么多概念, 来一个具体的实现. 实现过程其实很简单. 面试者把简历扔到一个盒子里, 然后面试官在合适的时机拿着盒子里的简历挨个打电话通知结果.
Events = function() { var listen, log, obj, one, remove, trigger, __this; obj = {}; __this = this; listen = function( key, eventfn ) { //把简历扔盒子, key就是联系方式. var stack, _ref; //stack是盒子 stack = ( _ref = obj[key] ) != null ? _ref : obj[ key ] = []; return stack.push( eventfn ); }; one = function( key, eventfn ) { remove( key ); return listen( key, eventfn ); }; remove = function( key ) { var _ref; return ( _ref = obj[key] ) != null ? _ref.length = 0 : void 0; }; trigger = function() { //面试官打电话通知面试者 var fn, stack, _i, _len, _ref, key; key = Array.prototype.shift.call( arguments ); stack = ( _ref = obj[ key ] ) != null ? _ref : obj[ key ] = []; for ( _i = 0, _len = stack.length; _i < _len; _i++ ) { fn = stack[ _i ]; if ( fn.apply( __this, arguments ) === false) { return false; } } return { listen: listen, one: one, remove: remove, trigger: trigger } } }
再来个成人电台的小栗子
var adultTv = Event(); adultTv .listen('play', function( data ){ alert ( "今天是谁的电影" + data.name ); }); //发布者 adultTv.trigger('play', {'name':'麻生希'});
命令模式(Command)
职责链模式(Chain of Responsibility)
MVC模式
MVC模式的基本理念,是通过把一个application封装成model, view和controller三个部分达到降低耦合,简化开发的目的。
举个栗子
<select id="selAnimal"> <option value="cat">cat</option> <option value="fish">fish</option> <option value="bird">bird</option> </select> <p id="whatDoesThisAnimalDo"></p> <script type="text/javascript"> document.getElementById('selAnimal').onchange = function() { var thisAnimalDoes; switch ( this.value ) { case 'cat': thisAnimalDoes = "cat meows"; break; case 'fish': thisAnimalDoes = "fish swims"; break; case 'bird': thisAnimalDoes = "bird flies"; break; default: thisAnimalDoes = "wuff?"; } document.getElementById('whatDoesThisAnimalDo').innerHTML = thisAnimalDoes; } </script>
这个小程序会把你从下拉菜单"selAnimal"中选择的动物能做什么事回显到网页上。以上是没有应用任何设计模式和编成思想写出的代码。如果要在这里应用MVC模式,代码又会变成怎样的呢?
<select id="selAnimal"> <option value="cat">cat</option> <option value="fish">fish</option> <option value="bird">bird</option> </select> <p id="whatDoesThisAnimalDo"></p> <script type="text/javascript"> // whatDoesAnimalDo 就是一个controller var whatDoesAnimalDo = { // 选择视图 start: function() { this.view.start(); }, // 将用户的操作映射到模型的更新上 set: function(animalName) { this.model.setAnimal(animalName); }, }; // whatDoesAnimalDo的数据model whatDoesAnimalDo.model = { // animal的数据 animalDictionary: { cat: "meows", fish: "swims", bird: "flies" }, // 当前的animal,也就是这个application的状态 currentAnimal: null, // 数据模型负责业务逻辑和数据存储 setAnimal: function(animalName) { this.currentAnimal = this.animalDictionary[animalName] ? animalName : null; this.onchange(); }, // 并且通知视图更新显示 onchange: function() { whatDoesAnimalDo.view.update(); }, // 还需要响应视图对当前状态的查询 getAnimalAction: function() { return this.currentAnimal ? this.currentAnimal + " " + this.animalDictionary[this.currentAnimal] : "wuff?"; } }; // whatDoesAnimalDo的视图 whatDoesAnimalDo.view = { // 用户输入触发onchange事件 start: function() { document.getElementById('selAnimal').onchange = this.onchange; }, // 该事件将用户请求发送给控制器 onchange: function() { whatDoesAnimalDo.set(document.getElementById('selAnimal').value); }, // 视图更新显示的方法,其中视图会向model查询当前的状态,并将其显示给用户 update: function() { document.getElementById('whatDoesThisAnimalDo').innerHTML = whatDoesAnimalDo.model.getAnimalAction(); } }; whatDoesAnimalDo.start(); </script>
突然一下代码变得好夸张e
偶都还没有在里面实现观察者模式呢
实在太不良好了。
这样就带出了MVC模式最大的诟病:在简单的系统中应用MVC模式,会增加结构的复杂性,并且降低效率。
与开发量相比,更加令人头疼的是效率的问题。三层之间的相互通信都是额外的开销。对于服务器端,这些通信造成的开销几乎可以忽略不计。但对于 javascript这样相对低效率的语言,多几重调用和事件侦听就能明显感觉到性能的下降了。而且,因为javascrip的闭包特性,一不小心就搞出 内存泄漏,这可是正宗的偷鸡不成蚀把米了
所以,对于javascript开发,适度开发可能比应用你所学到的学术知识更重要,毕竟前端开发是以解决实际问题为基础,而不是为了炫技。当然,Dflying gg有句话叫“重构无处不在”——如果你觉得你自己的代码越来越乱,维护起来越来越困难,那么,你就该考虑是不是该用MVC重构一下了~
再额外的说一句:是不是整个前端开发就不必使用MVC了呢?no no~ 其实整个前端开发就是一个大大的MVC架构啊——
- Model: HTML/XHTML中的信息
- View: Style sheet
- Controller: EMAScripts等等脚本
这不正是web标准的最终目的么
所以,把握好整个前端代码的结构,永远比在javascript开发中过量应用设计模重要的多!
文章来源:
http://kb.cnblogs.com/page/41674/
http://www.cnblogs.com/Darren_code/archive/2011/08/31/JavascripDesignPatterns.html
http://blog.jobbole.com/29454/