《JavaScript设计模式与开发实践》阅读摘要

《JavaScript设计模式与开发实践》作者:曾探

系统的介绍了各种模式,以及js中的实现、应用,以及超大量高质量代码,绝对值得一读


面向对象的js

静态类型:编译时便已确定变量的类型
优点:

编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行中可能发生的一些错误;编译器可以针对数据类型对程序进行一些优化工作;

缺点:

迫使程序员按照契约来编写;类型的声明会增加更多的代码;

动态类型:程序运行的时候,变量被赋予某个值之后,才会具有某种类型
优点:

编写的代码数量更少,看起来也更简洁,程序员可以把更多精力放在业务逻辑;给编码带来了很大的灵活性,无需进行类型检测,可以尝试调用任何对象的任意方法,无需考虑它原本是否被设计为拥有该方法,建立在鸭子类型上。

缺点:

无法保证变量的类型,从而在程序运行期可能发生跟类型相关的错误;

鸭子类型:

“如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子。”

鸭子类型指导我们只关注对象的行为,而不关注对象本身,即灌输HAS-A,而不是IS-A。利用鸭子类型的思想,不必借助超类型的帮助,就可以轻松实现:“面向接口编程,而不是面向实现编程。”例如:一个对象若有push和pop方法,并且提供了正确的实现,他就可以被当成栈来使用。

多态:
实际含义:

同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。

本质:

实际上时把“做什么”和“谁去做”分离开来,消除类型之间的耦合关系,js对象的多态性时与生俱来的。

作用:

把过程化的条件分支语句转化为对象的多态性,从而消除这些分支语句。

静态类型的多态:

通过向上转型:当给一个类变量赋值时,这个变量的类既可以使用这个类本身,也可以使用这个类的超类。使用继承来得到多态效果,是让对象表现出多态性的最常用手段:包括实现继承、接口继承。

js的多态:

js的变量类型在运行期是可变的,一个对象可以表示不同类型的对象,js对象的多态性是与生俱来的。

封装:
包含:

封装数据、封装实现、封装类型、封装变化。

封装数据:

通常是由语法解析实现(private、public、protected),js只能通过变量的作用域实现,并且只能模拟出public和private这两种封装性。

封装实现:

对象内部的变化对其他对象是透明不可见的;对象对它自己的行为负责;其他对象不关心它的内部实现;封装使得对象之间的耦合变松散,对象之间只通过暴露的API接口来通信。

封装类型:

静态语言中一种重要的封装方式,一般通过抽象类和接口来进行,把对象真正的类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。封装类型方面,js没有能力,也没有必要做得更多。

封装变化:

通过封装变化的方式,把系统中稳定不变的部分和容易改变的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易,这可以最大程度的保证程序的稳定性和可拓展性。

原型编程:

以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来。原型编程的思想中,类并不是必需的,对象是通过克隆另外一个对象得到的。

原型模式

定义:

既是一种设计模式也被称为一种编程范型。原型模式是用于创建对象的一种模式,不关心对象的具体类型,找到一个对象,通过克隆来创建一个一摸一样的对象。

实现关键:

语言本身是否提供了clone方法,es5提供了Object.create方法,可以用来克隆对象。

目的:

提供了一种便捷的方式去创建某个类型的对象。

原型继承的本质:

基于原型链的委托机制。

委托机制:

当对象无法响应某个请求时,会把该请求委托给它的原型。

原型编程范型基本规则:
  • 所有的数据都是对象
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  • 对象会记住它的原型
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型
js中的原型继承
所有的数据都是对象:

设计者本意,除了undefined之外,一切都应该是对象,所以存在“包装类”。js不能说所有的数据都是对象,但可以说绝大部分数据都是对象,js中存在Object.prototype对象,其他对象追根溯源都克隆于这个根对象,Object.prototype是它们的原型。

要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它:

js语言中,我们不需要关系克隆的细节,引擎内部负责实现,只要显示的调用var obj1 = new Object()或者var obj2 = {}。引擎内部会从Object.prototype上克隆一个对象出来。用new运算符来创建对象的多城,实际上也只是先克隆Object.prototype对象,再进行一些其他额外操作。

对象会记住它的原型:

js给对象提供了一个名为proto的隐藏属性,默认会指向它的构造器的原型对象,它就是对象跟它的原型联系起来的纽带。

如果对象无法响应某个请求,它会把这个请求委托给它的原型:

原型链查找

原型继承的未来:

设计模式在很多时候都体现了语言的不足之处


this、call、和apply

this:

总是指向一个对象,而具体指向哪个对象是在运行时基于执行环境动态绑定的,而非函数被声明时的环境。

this的指向:
  • 作为对象的方法调用
  • 作为普通的函数调用
  • 构造器调用
  • Function.prototype.call或Function.prototype.apply调用
  1. 作为对象的方法被调用时,this指向该对象
  2. 作为普通函数调用,this总是指向全局对象,在浏览器中全局对象为window,在node.js中全局对象为global,严格模式下为undefined
  3. 构造器调用,this通常情况下指向返回的对象
  4. Function.prototype.call或Function.prototype.apply调用动态的绑定this到传入的第一个参数
call和apply的区别:

传入参数形式不同,它们第一个参数都是指定函数体内this对象的指向,apply第二个参数为一个带下表的集合,可以是数组或者类数组,call第二个参数开始,每个参数依次被传入函数。apply比call的使用率更高,call是包装在apply上面的语法糖,如果我们明确的知道函数接受多少个参数,并且想一目了然地表达形参和实参的对应关系,适合使用call来传送。

call和apply的用途:
  1. 改变this的指向
  2. Function.prototype.bind:
Function.prototype.bind = function ( context ) {
       var self = this;
        return function () {
           return self.apply( context, arguments );
        }
};
  1. 借用其他对象的方法:借用构造函数、对类数组甚至对象(对象本身要可以存取属性、length属性可读写)使用数组的方法

闭包和高阶函数

js是一门完整的面向对象的编程语言,同时也拥有许多函数式语言的特性。

变量的作用域:

变量的有效范围,在函数声明变量时没有带关键字var就会变成全局变量,使用了var时称为局部变量,只有在该函数内部才能访问到这个变量,在函数外面时访问不到的。js中函数可以用来创造函数作用域。在函数里面可以看到外面的变量,而在函数外面无法看到函数里面的变量,这是因为在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。

变量的生存周期:

全局变量的生存周期是永久的,除非主动销毁。在函数内使用var声明的局部变量,在函数退出时,这些局部变量记失去了它们的价值,会随着函数调用的结束而被销毁。

闭包的作用:

因为对外部作用域的引用可以阻止外部的作用域被销毁,延长了局部变量的生命周期、可以把每次循环中的i值都封闭起来,使循环绑定事件符合我们的预期

闭包的更多作用:
封装变量:

提炼函数时代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,常常把这些代码块封装在独立的小函数里面。独立的小函数有助于代码复用,如果有良好的命名,本身也起到了注释的效果,如果这些小函数不需要在程序的其他地方使用,最好是把它们用闭包封闭起来。

闭包和面向对象设计:

对象以方法的形式包括了过程,闭包在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能实现,反之亦然。

用闭包实现命令模式:

命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。

闭包与内存管理:

解决对象间循环引用带来的内存泄漏问题,只需要把循环引用中的变量设为null即可。将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器瑕疵运行时,就会删除这些值并回收它们占用的内存

高阶函数:
定义:

满足下列条件之一的函数:

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出
函数作为参数传递:

这代表着我们可以抽离一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样依赖可以分离业务代码中变化与不变的部分。例如:

  1. 回调函数
    异步请求、当一个函数不适合执行一些请求时,可以把这些请求封装成一个函数,并把它作为参数传递给另一个函数,“委托”给另外一个函数来执行。
  2. Array.prototype.sort
    Array.prototype.sort接受一个函数当作参数,这个函数里封装了数组元素的排序规则。从Array.prototype.sort的使用可以看到,我们的目的是对数组进行排序,这是不变的部分;从而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里,动态传入,使这个方法称为了一个非常灵活的方法。
函数作为返回值输出:
  1. 判断数据的类型
var isType = function(type){
    return function( obj ) {
    return Object.prototype.toString.call( obj ) === ‘[object ‘ + type + ‘]’;
    }
};
var isString = isType(‘String’);
var isArray = isType(‘Array’);
var isNumber = isType(‘Number’);

2.getSingle

var getSingle = function ( fn ) {
    var ret;
    return function ( ) {
        return ret || (ret = fn.apply ( this, arguments ) );
    };
};
高阶函数实现AOP

AOP(面向切面编程)的主要作用是吧一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。优点首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便的复用日志统计等功能模块。js中实现AOP更简单,这是js与生俱来的能力,这种使用AOP的方式给函数添加职责,也是js语言中一种非常特别和巧妙的装饰者模式实现。

高阶函数的其他应用:
  1. currying:
    currying又称部分求职。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求职,而是继续返回另外一个函数,刚才传入的参数在函数形成闭包中被保存起来。待到函数真正需要求职的时候,之前传入的所有参数都会被一次性用于求值。
  2. uncurrying:
    js中,当我们调用对象的某个方法时,其实不用去关系改对象原本是否被设计为拥有这个方法,这是动态语言的特点,也是常说的鸭子类型思想。
  3. 函数节流:
    函数被频繁调用的场景:window.onresize事件、mousemove事件、上传进度
    函数节流的原理:借助setTimeout来完成
    函数节流的代码实现:
var throttle = function ( fn, interval ) {
    var _self = fn,
          timer,
          firstTime = true;

    return function () {
          var args = arguments,
          _me = this;

        if ( fisrtTime ) {
            _self.apply( _me, args );
            return firstTime = false;
        }

        if ( timer ) {
            return false;
        }

        timer = setTimeout ( function ( ) [
            clearTimeout ( timer );
            timer = null;
            _self.apply ( _me, args );

        }, interval || 500 );
    };
};
window.onresize = throttle ( function ( ) {
    console.log ( 1 );
}, 500 );
  1. 分时函数
    使用函数、定时器让一个大任务分成多个小任务
  2. 惰性加载函数
    在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是符合当前浏览器环境的函数。

单例模式

实现一些只需要一个的对象,比如线程池、全局缓存、window对象等

实现单例模式:

用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。

透明的单例模式:

用户从这个类中创建对象的时候,可以像使用其他任何普通的类一样。

用代理实现单例模式:
    var CreateDiv = function (html) {
        this.html = html;
        this.init();
    };

    CreateDiv.prototype.init = function () {
        var div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
    };

    var ProxySingleCreateDiv = (function () {
        var instance;
        return function (html) {
            if (!instance) {
                instance = new CreateDiv(html);
            }
            return instance;
        }
    })();
    var a = new ProxySingleCreateDiv('seven1');
    var b = new ProxySingleCreateDiv('seven2');
    alert(a === b);
js中的单例模式:

可以将全局变量当作单例模式来使用,但是全局变量会污染命名空间。可以使用以下几种方法避免全局空间的污染:

  1. 使用命名空间
    不会杜绝全局变量,可以减少全局变量的数量。使用对象字面量的方式。
  2. 使用闭包封装私有变量
    把变量封装在闭包的内部,只暴露一些借口跟外界通信。
惰性单例

惰性单例是指在需要的时候才创建对象实例。

通用的惰性单例
var getSingle = function( fn ) {
    var result ;
    return function ( ) {
        return result || ( result = fn.apply ( this, arguments ) );
    } 
};
~~~
***
####策略模式
实现一个功能有多个方案可以选择
#####定义:
定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。
#####策略模式的程序组成:
第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。

#####js版本的策略模式:
在js语言中,函数也是对象,所以更简单和直接的做法是把策略和Context定义成一个函数。

#####多态在策略模式中的体现:
所有跟算法有关的逻辑不再放在Context中,而是分布在各个策略队形中。当我们对这些策略对象发出请求时,它们会返回各自不同的结果,这正是对象多态性的体现,也是“它们可以互相替换”的目的。

#####使用策略模式实现缓动动画
原理:js实现动画效果的原理跟动画片的制作一样,js通过连续改变元素的某个CSS属性,来实现动画效果。
思路和准备工作:
运动之前,需要记录一些有用的信息,至少包括:
* 动画开始时,小球所在的原始位置;
* 小球移动的目标位置
* 动画开始时的准确时间点
* 小球运动的持续时间

通过定时器,把动画已消耗的时间、小球原始位置、小球目标位置和动画持续的总时间传入缓动算法。该算法会通过这几个参数,计算出小球当前应该所在的位置。最后再更新该div的CSS属性,小球就能顺利的动起来了。
~~~
var Animate = function( dom ){
        this.dom = dom; // 进行运动的dom 节点
        this.startTime = 0; // 动画开始时间
        this.startPos = 0; // 动画开始时,dom 节点的位置,即dom 的初始位置
        this.endPos = 0; // 动画结束时,dom 节点的位置,即dom 的目标位置
        this.propertyName = null; // dom 节点需要被改变的css 属性名
        this.easing = null; // 缓动算法
        this.duration = null; // 动画持续时间
    };
    Animate.prototype.start = function( propertyName, endPos, duration, easing ){
        this.startTime = +new Date; // 动画启动时间
        this.startPos = this.dom.getBoundingClientRect()[ propertyName ]; // dom 节点初始位置
        this.propertyName = propertyName; // dom 节点需要被改变的CSS 属性名
        this.endPos = endPos; // dom 节点目标位置
        this.duration = duration; // 动画持续事件
        this.easing = tween[ easing ]; // 缓动算法
        var self = this;
        var timeId = setInterval(function(){ // 启动定时器,开始执行动画
            if ( self.step() === false ){ // 如果动画已结束,则清除定时器
                clearInterval( timeId );
            }
        }, 19 );
    };

    Animate.prototype.step = function(){
    var t = +new Date; // 取得当前时间
    if ( t >= this.startTime + this.duration ){ // (1)
        this.update( this.endPos ); // 更新小球的CSS 属性值
        return false;
    }
    var pos = this.easing( t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration );
    // pos 为小球当前位置
        this.update( pos ); // 更新小球的CSS 属性值
    };

    Animate.prototype.update = function( pos ){
        this.dom.style[ this.propertyName ] = pos + 'px';
    };

    var div = document.getElementById( 'div' );
    var animate = new Animate( div );
    animate.start( 'left', 500, 1000, 'strongEaseOut' );
~~~
#####更广义的“算法”
把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”,只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们。

#####策略模式的优点
* 策略模式利用组合、委托、和多态等技术和思想,可以有效地避免多重条件选择语句
* 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立strategy中,使得它们易于切换,易于理解,易于扩展
* 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制和粘贴工作
* 在策略模式中利用组合和委托让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案

#####策略模式的缺点:
首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆在Context中要好
其次,要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy。

#####一等函数对象与策略模式:
在js中除了使用类来封装算法和行为之外,使用函数当然也是一种选择。这些“算法”可以被封装在函数中并且四处传递,也就是我们常说的“高阶函数”
***
####代理模式
代理模式是为一个对象提供一个待用品或占位符,以便控制对它的访问。
#####现实场景例子:
明星的经纪人代替明星协商。
#####保护代理和虚拟代理:
控制不同权限的对象对目标对象的访问,叫作保护代理;把一些开销很大的对象,延迟到真正需要它的时候再去创建,叫作虚拟代理。js中不容易实现保护代理,虚拟代理是最常用的一种代理模式。

#####代理的意义:
######单一职责原则:
一个类(也包括对象和函数)应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,面向对象设计鼓励将行为分布到颗粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

######代理和本体接口的一致性:
优点:
* 用户可以放心地请求代理,他只关心能否得到想要的结果
* 在任何使用本体的地方都可以替换成使用代理

#####虚拟代理实现图片加载
~~~
    var myImage = (function(){
        var imgNode = document.createElement( 'img' );
        document.body.appendChild( imgNode );
        return function( src ){
            imgNode.src = src;
        }
    })();
    var proxyImage = (function(){
        var img = new Image;
        img.onload = function(){
            myImage( this.src );
        }
        return function( src ){
            myImage( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
            img.src = src;

        }
    })();
    proxyImage( 'http:// imgcache.qq.com/music// N/k/000GGDys0yA0Nk.jpg' );
~~~
#####缓存代理:
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面的运算结果。

#####其他代理模式:
* 防火墙代理
* 远程代理
* 保护代理
* 智能引用代理
* 写时复制代理(虚拟代理的变体)
***
####迭代器模式:
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
#####例子:Array.prototype.forEach
#####内部迭代器和外部迭代器:
* 内部迭代器:each函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只要一次引用。缺点:迭代规则已经被提前规定,无法灵活更改
* 外部迭代器:必须显式地请求迭代下一个元素,增加了调用的复杂度,但也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。

#####迭代类数组对象和字面量对象:
无论内部迭代器还是外部迭代器,只要迭代的聚合对象拥有length属性而且可以用下标访问,那它就可以被迭代。
~~~
    var each = function( ary, callback ){
        for ( var i = 0, l = ary.length; i < l; i++ ){
            if ( callback( i, ary[ i ] ) === false ){ // callback 的执行结果返回false,提前终止迭代
                break;
            }
        }
    };

    each( [ 1, 2, 3, 4, 5 ], function( i, n ){
        if ( n > 3 ){ // n 大于3 的时候终止循环
            return false;
        }
        console.log( n ); // 分别输出:1, 2, 3
    });
~~~
#####发布—订阅模式(观察者模式)
无论MVC还是MVVM都少不了发布-订阅模式,js本身也是一门基于事件驱动的语言。
#####现实场景例子:把电话留给售楼处,一旦有新房会电话通知。
#####优点:
时间上解耦、对象之间解耦。
#####缺点:
创建订阅者本身要消耗一定的时间和内存、过度使用导致对象和对象之间的必要联系也将被深埋导致程序难以维护和理解
#####作用:
* 可以广泛应用于异步编程中,代替传递回调函数。通过订阅事件可以忽略运行期间的状态,只需要关注事件本身。
* 取代对象之间硬编码的通知机制,一个对象不用显式地调用另一个对象的某个接口。让两个对象松耦合地联系在一起,虽然不清楚彼此间的细节,但这不影响它们之间相互通信。
#####js实现发布-订阅模式的便利性:
注册回调函数代替传统的发布-订阅模式,更加优雅、简单
~~~
//所以我们把发布—订阅的功能提取出来,放在一个单独的对象内:
var event = {
    clientList: [],
    listen: function( key, fn ){
        if ( !this.clientList[ key ] ){
            this.clientList[ key ] = [];
        }
            this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表
        },
        trigger: function(){
            var key = Array.prototype.shift.call( arguments ), // (1);
            fns = this.clientList[ key ];
            if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
                return false;
            }
            for( var i = 0, fn; fn = fns[ i++ ]; ){
                fn.apply( this, arguments ); // (2) // arguments 是trigger 时带上的参数
            }
        }
    };

    var installEvent = function( obj ){
        for ( var i in event ){
            obj[ i ] = event[ i ];
        }
    };
~~~
***
####命令模式
最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。
#####现实场景例子:点菜。
#####应用场景:
有时需要向某些对象发送请求,但是不知道请求的接受者是谁,也不知道请求的操作是什么。
#####js中的命令模式:
js作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了js语言中,可以用高阶函数非常方便的实现命令模式,是一种隐式的模式。
~~~
        var setCommand = function( button, func ){
            button.onclick = function(){
                func();
            }
        };
        var MenuBar = {
            refresh: function(){
                console.log( '刷新菜单界面' );
            }
        };
        var RefreshMenuBarCommand = function( receiver ){
            return function(){
                receiver.refresh();
            }
        };
        var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );
        setCommand( button1, refreshMenuBarCommand );
~~~
#####智能命令与傻瓜命令:
一般来说命令模式都会在command对象中保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接受者来执行,这种模式的好处是请求发起者和接受者之间尽可能地得到了解耦。“聪明”的命令对象可以直接实现请求,这样以来就不再需要接受者的存在,这种“聪明”的命令对象也叫作智能命令。
***
####组合模式
#####含义:
用小的子对象构建更大的对象,这些小的子对象本身也许是由更小的对象构成的。
#####用途:
1. 表示树形结构,非常方便的描述对象部分-整体层次结构
2. 利用对象多态性统一对待组合对象和单个对象

#####一些值得注意的地方
1. 组合模式不是父子关系
2. 对叶对象操作的一致性
3. 双向映射关系
4. 用职责链提高组合模式性能

~~~
<html>
<body>
    <button id="button">按我</button>
</body>
<script>
    var MacroCommand = function(){
        return {
            commandsList: [],
            add: function( command ){
                this.commandsList.push( command );
            },
            execute: function(){
                for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                    command.execute();
                }
            }
        }
    };
    var openAcCommand = {
        execute: function(){
            console.log( '打开空调' );
        }
    };
/**********家里的电视和音响是连接在一起的,所以可以用一个宏命令来组合打开电视和打开音响的命令
*********/
var openTvCommand = {
    execute: function(){
        console.log( '打开电视' );
    }
};
var openSoundCommand = {
    execute: function(){
        console.log( '打开音响' );
    }
};
var macroCommand1 = MacroCommand();
macroCommand1.add( openTvCommand );
macroCommand1.add( openSoundCommand );
/*********关门、打开电脑和打登录QQ 的命令****************/
var closeDoorCommand = {
    execute: function(){
        console.log( '关门' );
    }
};
var openPcCommand = {
    execute: function(){
        console.log( '开电脑' );
    }
};
var openQQCommand = {
    execute: function(){
        console.log( '登录QQ' );
    }
};
var macroCommand2 = MacroCommand();
macroCommand2.add( closeDoorCommand );
macroCommand2.add( openPcCommand );
macroCommand2.add( openQQCommand );
/*********现在把所有的命令组合成一个“超级命令”**********/
var macroCommand = MacroCommand();
macroCommand.add( openAcCommand );
macroCommand.add( macroCommand1 );
macroCommand.add( macroCommand2 );
/*********最后给遥控器绑定“超级命令”**********/
var setCommand = (function( command ){
    document.getElementById( 'button' ).onclick = function(){
        command.execute();
    }
})( macroCommand );
</script>
</html>
~~~

***
####模版方法模式
#####定义:
一种只需要使用继承就可以实现的非常简单的模式
~~~
    var Coffee = function(){};
    Coffee.prototype = new Beverage();

    Coffee.prototype.brew = function(){
        console.log( '用沸水冲泡咖啡' );
    };
    Coffee.prototype.pourInCup = function(){
        console.log( '把咖啡倒进杯子' );

    };
    Coffee.prototype.addCondiments = function(){
        console.log( '加糖和牛奶' );
    };
    var Coffee = new Coffee();
    Coffee.init();

    Beverage.prototype.init = function(){
        this.boilWater();
        this.brew();
        this.pourInCup();
        this.addCondiments();
    };

    var Tea = function(){};
    Tea.prototype = new Beverage();
    Tea.prototype.brew = function(){
        console.log( '用沸水浸泡茶叶' );
    };
    Tea.prototype.pourInCup = function(){
        console.log( '把茶倒进杯子' );
    };
    Tea.prototype.addCondiments = function(){
        console.log( '加柠檬' );
    };
    var tea = new Tea();
    tea.init();
~~~
#####组成:
由两部分结构组成:第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
#####抽象类:
不应被实例化,用来被某些具体类继承的。用于向上转型、为子类定义公共接口。
#####抽象方法:
没有具体的实现过程,当子类继承这个抽象类时,必须重写抽象方法
#####具体方法:
具体实现方法 

#####js没有抽象类的缺点和解决方案
抽象类的一个作用时隐藏对象的具体类型,因为js时一门“类型模糊”的语言,所以隐藏对象在js中并不总要。使用原型继承来墨迹传统的类继承时,并没有编译器帮助我们进行任何形式的检查,我们也没有办法保证子类会重写父类中的“抽象方法”
######解决方案:
* 第一种方案:使用鸭子类型来模拟设备接口检查,以便确保子类中确实重写了父类的方法;
* 第二种方案:让父类的方法直接抛出一个异常,入股因为粗心忘记改写,至少我们会在程序运行时得到一个错误。

#####钩子方法:防止钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子有一个默认的实现,究竟要不要“挂钩“,由子类自行决定

#####好莱坞原则:
允许底层组件将自己挂钩到高层组件中,高层组件会决定什么时候、以何种方式去使用这些底层组件。模版方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,当我们用模版方法编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。还适用于其他模式和场景,例如发布—订阅模式和回调函数。
***
####享元(flyweight)模式
一种用于性能优化的模式,fly在这里是苍蝇的意思,意为蝇量级,核心是运用共享技术来有效支持大量细粒度的对象。
#####现实场景例子:模特换不同的衣服拍照。
#####内部状态和外部状态:
* 内部状态存储于对象内部
* 内部状态可以被一些对象共享
* 内部状态独立于具体的场景,通常不会改变
* 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享

#####享元模式的适用性
* 一个程序使用了大量的相似对象
* 由于使用了大量对象,造成很大的内存开销
* 对象的大多数状态都可以变成外部状态
* 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象

#####享元模式的关键:
把内部状态和外部状态分离开来。有多少内部状态的组合,系统便最多存在多少个共享对象,而外部状态则储存在共享对象的外部,在必要时传入共享对象来组装成一个完整的对象。
也可以用对象池+事件委托来代替实现
~~~
var Upload = function( uploadType){
        this.uploadType = uploadType;
    };

    Upload.prototype.delFile = function( id ){
        uploadManager.setExternalState( id, this ); // (1)
        if ( this.fileSize < 3000 ){
            return this.dom.parentNode.removeChild( this.dom );
        }

        if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){
            return this.dom.parentNode.removeChild( this.dom );
        }
    }


    var UploadFactory = (function(){
        var createdFlyWeightObjs = {};
        return {
            create: function( uploadType){
                if ( createdFlyWeightObjs [ uploadType] ){
                    return createdFlyWeightObjs [ uploadType];
                }
                return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
            }
        }
    })();

    var uploadManager = (function(){
        var uploadDatabase = {};
        return {
            add: function( id, uploadType, fileName, fileSize ){
                var flyWeightObj = UploadFactory.create( uploadType );
                var dom = document.createElement( 'div' );
                dom.innerHTML =
                '<span>文件名称:'+ fileName +', 文件大小: '+ fileSize +'</span>' +
                '<button class="delFile">删除</button>';
                dom.querySelector( '.delFile' ).onclick = function(){
                    flyWeightObj.delFile( id );
                }

                document.body.appendChild( dom );
                uploadDatabase[ id ] = {
                    fileName: fileName,
                    fileSize: fileSize,
                    dom: dom
                };
                return flyWeightObj ;
            },
            setExternalState: function( id, flyWeightObj ){
                var uploadData = uploadDatabase[ id ];
                for ( var i in uploadData ){
                    flyWeightObj[ i ] = uploadData[ i ];
                }
            }
        }
    })();

    var id = 0;
    window.startUpload = function( uploadType, files ){
        for ( var i = 0, file; file = files[ i++ ]; ){
            var uploadObj = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize );
        }
    };
~~~
***
####职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
#####现实场景例子:
公交车人太多了,找不到售票员,通过一个个人将钱递给售票员
#####例子:
作用域链、原型链、dom节点事件冒泡
#####最大优点:
请求发送者只需要知道链中的第一个节点,从而弱化了发送者和接受者之间的强联系。
#####缺点:
使程序中多了一些节点对象,可能在某一次请求传递的过程中,大部分节点没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能损耗
#####小结:
js开发中,职责链模式是最容易被忽视的模式之一。职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间耦合性。职责链汇中的节点数量和顺序是可以自由变化的。
~~~
var order500 = function( orderType, pay, stock ){
        if ( orderType === 1 && pay === true ){
            console.log( '500 元定金预购,得到100 优惠券' );
        }else{
            return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
        }
    };

    var order200 = function( orderType, pay, stock ){
        if ( orderType === 2 && pay === true ){
            console.log( '200 元定金预购,得到50 优惠券' );
        }else{
            return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
        }
    };

    var orderNormal = function( orderType, pay, stock ){
        if ( stock > 0 ){
            console.log( '普通购买,无优惠券' );
        }else{
            console.log( '手机库存不足' );
        }
    };

    // Chain.prototype.setNextSuccessor 指定在链中的下一个节点
    // Chain.prototype.passRequest 传递请求给某个节点
    var Chain = function( fn ){
        this.fn = fn;
        this.successor = null;
    };

    Chain.prototype.setNextSuccessor = function( successor ){
        return this.successor = successor;
    };

    Chain.prototype.passRequest = function(){

        var ret = this.fn.apply( this, arguments );
        if ( ret === 'nextSuccessor' ){
            return this.successor && this.successor.passRequest.apply( this.successor, arguments );
        }
        return ret;
    };
~~~
***
####中介者模式
#####作用:
解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都可以通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。
#####小结:
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,在中介者模式中,对象之间几乎不知道彼此的存在,它们只通过中介者对象来互相影响对方。
#####缺点:
会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的的。中介者对象自身往往是难以维护的对象。
一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随着项目的变化呈现指数增长曲线,那我们就可以考虑使用中介者模式来重构代码。
***
####装饰者模式:
装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响这个类中派生的其他对象。
~~~
var plane = {
        fire: function(){
            console.log( '发射普通子弹' );
        }
    }
    var missileDecorator = function(){
        console.log( '发射导弹' );
    }
    var atomDecorator = function(){
        console.log( '发射原子弹' );
    }
    var fire1 = plane.fire;
    plane.fire = function(){
        fire1();
        missileDecorator();
    }
    var fire2 = plane.fire;
    plane.fire = function(){
        fire2();
        atomDecorator();
    }
    plane.fire();
    // 分别输出: 发射普通子弹、发射导弹、发射原子弹
~~~
#####装饰者模式和代理模式:
主要区别在于它们的意图和设计目的。
***
####状态模式
状态模式的关键是区分事物内部的状态,事物内部的状态的改变往往会带来事物的行为的改变。
#####关键:
把事物的每种状态都封装成单独的类,跟此种状态相关的行为都封装在类中
#####优点:
* 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
* 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
* 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然
* Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响

#####缺点:
会在系统中定义许多状态类,编写20个状态类是一件枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方久看出整个状态机的逻辑
#####性能优化点:
* 有两种选择来管理state对象的创建和销毁。第一种是仅当state对象被需要时才创建并随后销毁,另一种是开始久创建好所有的状态对象,并且始终不销毁它们。第一种可以节省内存,第二种适用于状态切换很快
* 各个Context对象可以共享一个state对象,这也是享元模式的应用场景之一。

~~~
var Light = function(){
        this.offLightState = new OffLightState( this ); // 持有状态对象的引用
        this.weakLightState = new WeakLightState( this );
        this.strongLightState = new StrongLightState( this );
        this.superStrongLightState = new SuperStrongLightState( 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();
        }
    };

    var OffLightState = function( light ){
        this.light = light;
    };

    OffLightState.prototype.buttonWasPressed = function(){
        console.log( '弱光' );
        this.light.setState( this.light.weakLightState );
    };
~~~
***
####适配器模式
适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能如何演化。适配器模式不需要改变已有的接口,就能把它们协同作用。
#####现实场景例子:
充电适配器
#####和其他相似模式的比较:
装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常会刑场一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。
~~~
    var googleMap = {
        show: function(){
            console.log( '开始渲染谷歌地图' );
        }
    };
    var baiduMap = {
        display: function(){
            console.log( '开始渲染百度地图' );
        }
    };
    var baiduMapAdapter = {
        show: function(){
            return baiduMap.display();

        }
    };

    renderMap( googleMap ); // 输出:开始渲染谷歌地图
    renderMap( baiduMapAdapter ); // 输出:开始渲染百度地图
~~~
***
####单一职责原则(SRP)
单一职责原则的职责被定义为“引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。SRP原则体现为:一个对象(方法)只做一件事情。
#####运用:
代理模式、迭代器模式、单例模式和装饰者模式
#####何时应该分离职责:
如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们;职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离液不迟
#####优点:
降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,有助于代码的附庸,也有利于单元测试。当一个职责需要变更的时候,不会影响到其他职责。
#####缺点:
增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。
***
####最少知识量原则(LKP)
最少知识原则也叫迪米特法则
一个软件应用应当尽可能少地与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统给、类、模块、函数、变量等。
#####应用:
中介者模式、外观模式(为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使子系统更佳容易使用)
***
####开放—封闭原则(OCP)
软件实体(类、模块、函数)等应该是可以拓展的,但是不可修改
#####实现:
利用对象的多态、放置挂钩、使用回调函数
#####应用:
发布-订阅模式、模版方法模式、策略模式、代理模式、职责链模式
***
####接口和面向接口编程
接口是对象能响应的请求的集合
***
####代码重构
#####提炼函数
* 避免出现超大函数
* 独立出来的函数有助于代码复用
* 独立出来的函数更容易被覆写
* 独立出来的函数如果拥有一个良好的命名,它本身就起到了注释的作用

#####合并重复的条件片段
#####把条件分支语句提炼成函数
#####合理使用循环
#####提前让函数推出嵌套条件分支
#####传递对象参数代替过长的参数列表
#####尽量减少参数数量
#####少用三目运算符
#####合理使用链式调用
#####分解大型类
#####用return退出多重循环
***
感谢您耐心看完,求赞、关注,☺

推荐阅读更多精彩内容

  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 6,531评论 2 17
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 156,024评论 24 681
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 2,988评论 1 15
  • 不知道是什么让我坚持到现在。也许是懒惰,也许是每一次的软弱。 每次争吵都会特别恨自己,恨自己一次次的忘记,容忍。都...
    在人群里来来往往阅读 103评论 0 1
  • 今天我们去给老爷爷老奶奶上了坟,每年的大年初一去看看他们,是我们家的一个习惯,希望这个传统能在你这里保持下去。 每...
    思想的猪阅读 195评论 0 0