《JavaScript设计模式与开发实践》读书笔记

93 阅读21分钟

设计模式

在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。

通俗一点说,设计模式是在某种场合下对某个问题的一种解决方案。如果再通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。

设计模式的主题总是把不变的事物和变化的事物分离开来

JavaScript是动态类型语言

静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时 候,待变量被赋予某个值之后,才会具有某种类型。

优点是编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地放在业务逻辑上面。

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

JavaScript将函数作为一等对象,函数本身也是对象,函数用来封装行为并且能够被四处传递。

面向对象

设计原则

1. 单一职责原则(SRP)

就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。
一个对象(方法)只做一件事情。

应用场景:
代理模式、迭代器模式、单例模式和装饰者模式。

何时应该分离职责
一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在ajax 请求的时候,创建xhr 对象和发送xhr 请求几乎总是在一起的,那么创建xhr 对象的职责和发送xhr 请求的职责就没有必要分开。
另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟。

优点
降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。

缺点
会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。

2. 最少知识原则(LKP)

一个软件实体应当尽可能少地与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。
也叫迪米特法则(Law of Demeter,LoD)

应用场景:
中介者模式和外观模式

3. 开放-封闭原则

在面向对象的程序设计中,开放-封闭原则(OCP)是最重要的一条原则。

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。

几乎所有的设计模式都是遵守开放-封闭原则。

4. 里氏替换原则

所有引用基类(父类)的地方必须能透明地使用其子类的对象。

5. 依赖倒置原则

抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

6. 接口隔离原则

使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

特点

1. 多态

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

多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与 “可能改变的事物”分离开来。

把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放—封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。

多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
看如下例子:
条件分支语句:

var googleMap = {
    show: function(){
        console.log( '开始渲染谷歌地图' );
    }
};
var baiduMap = {
    show: function(){
        console.log( '开始渲染百度地图' );
    }
};
var renderMap = function( type ){
    if ( type === 'google' ){
        googleMap.show();
    }else if ( type === 'baidu' ){
        baiduMap.show();
    }
};
renderMap( 'google' ); // 输出:开始渲染谷歌地图
renderMap( 'baidu' ); // 输出:开始渲染百度地图

对象的多态性:

var renderMap = function( map ){
    if ( map.show instanceof Function ){
        map.show();
    }
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMap ); // 输出:开始渲染百度地图

2. 封装

封装的目的是将信息隐藏。一般而言,我们讨论的封装是封装数据和封装实现。更广义的封装,还包括封装类型和封装变化。

  • 封装类型
    抽象类和接口

把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。

  • 封装变化
    《设计模式》一书中共归纳总结了23 种设计模式。从意图上区分,这23 种设计模式分别被划分为创建型模式、结构型模式和行为型模式。
    创建型模式的目的就是封装创建对象的变化。
    而结构型模式封装的是对象之间的组合关系。
    行为型模式封装的是对象的行为变化。

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

3. 继承

高阶函数

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

应用场景:

  1. 实现AOP(面向切面编程)
    把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。
Function.prototype.before = function( beforefn ){
    var __self = this; // 保存原函数的引用
    return function(){ // 返回包含了原函数和新函数的"代理"函数
        beforefn.apply( this, arguments ); // 执行新函数,修正this
        return __self.apply( this, arguments ); // 执行原函数
    }
};
Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};
var func = function(){
    console.log( 2 );
};
func = func.before(function(){
    console.log( 1 );
}).after(function(){
    console.log( 3 );
});
func();
  1. currying函数柯里化

实质就是使用闭包返回一个延迟执行函数

其好处主要在于:

  1. 参数复用,或者说是固定参数,避免重复传参;
  2. 提前返回,或者说是提前确认,避免重复判断;
  3. 延迟执行。
var currying = function( fn ){
    var args = [];
    return function(){
        if ( arguments.length === 0 ){
            return fn.apply( this, args );
        }else{
            [].push.apply( args, arguments );
            return arguments.callee;
        }
    }
};
var cost = (function(){
    var money = 0;
    return function(){
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            money += arguments[ i ];
        }
        return money;
    }
})();
var cost = currying( cost ); // 转化成currying 函数
cost( 100 ); // 未真正求值
cost( 200 ); // 未真正求值
cost( 300 ); // 未真正求值
alert ( cost() ); // 求值并输出:600
  1. uncurrying

扩大函数的适用性,使本来作为特定对象所拥有的功能的函数可以被任意对象所用.

反柯里化并不是作为柯里化函数的反函数而存在,仅仅只是名字上存在关联。

反柯里化的作用类似于借用方法。柯里化可以提前绑定函数调用的上下文(也就是 this 关键字的指向),而反柯里化的作用之一就是解藕出一个绑定好的上下文的方法,听起来好像就是 Function.prototype.call。

把泛化this 的过程提取出来

Function.prototype.uncurrying = function () {
    var self = this;
    return function() {
        var obj = Array.prototype.shift.call( arguments );
        return self.apply( obj, arguments );
    };
};

var push = Array.prototype.push.uncurrying();
(function(){
    push( arguments, 4 );
    console.log( arguments ); // 输出:[1, 2, 3, 4]
})( 1, 2, 3 );
  1. 函数节流
var throttle = function ( fn, interval ) {
    var __self = fn, // 保存需要被延迟执行的函数引用
        timer, // 定时器
        firstTime = true; // 是否是第一次调用
    return function () {
        var args = arguments,
            __me = this;
        if ( firstTime ) { // 如果是第一次调用,不需延迟执行
            __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. 分时函数
var timeChunk = function( ary, fn, count ){
    var obj,
        t;
    var len = ary.length;
    var start = function(){
        for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){
            var obj = ary.shift();
            fn( obj );
        }
    };
    return function(){
        t = setInterval(function(){
            if ( ary.length === 0 ){ // 如果全部节点都已经被创建好
                return clearInterval( t );
            }
            start();
        }, 200 ); // 分批执行的时间间隔,也可以用参数的形式传入
    };
};

var ary = [];
for ( var i = 1; i <= 1000; i++ ){
    ary.push( i );
};
var renderFriendList = timeChunk( ary, function( n ){
    var div = document.createElement( 'div' );
    div.innerHTML = n;
    document.body.appendChild( div );
}, 8 );
renderFriendList();
  1. 惰性加载函数
var addEvent = (function(){
    if ( window.addEventListener ){
        return function( elem, type, handler ){
            elem.addEventListener( type, handler, false );
        }
    }
    if ( window.attachEvent ){
        return function( elem, type, handler ){
            elem.attachEvent( 'on' + type, handler );
        }
    }
})();

目前的addEvent 函数依然有个缺点,也许我们从头到尾都没有使用过addEvent 函数,这样看来,前一次的浏览器嗅探就是完全多余的操作,而且这也会稍稍延长页面ready 的时间。

var addEvent = function( elem, type, handler ){
    if ( window.addEventListener ){
        addEvent = function( elem, type, handler ){
            elem.addEventListener( type, handler, false );
        }
    }else if ( window.attachEvent ){
        addEvent = function( elem, type, handler ){
            elem.attachEvent( 'on' + type, handler );
        }
    }
    addEvent( elem, type, handler );
};

设计模式

JavaScript设计模式.png

1. 原型模式

如果我们想要创建一个对象,一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式,我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。

var Plane = function(){
    this.blood = 100;
    this.attackLevel = 1;
    this.defenseLevel = 1;
};
var plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;
var clonePlane = Object.create( plane );

在不支持Object.create 方法的浏览器中,则可以使用以下代码:

Object.create = Object.create || function( obj ){
    var F = function(){};
    F.prototype = obj;
    return new F();
}

JavaScript 本身是一门基于原型的面向对象语言,它的对象系统就是使用原型模式来搭建的,在这里称之为原型编程范型也许更合适。

原型编程范型至少包括以下基本规则:

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

2. 单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的window 对象等。在JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

用代理实现单例模式

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 ProxySingletonCreateDiv = (function(){
    var instance;
    return function( html ){
        if ( !instance ){
            instance = new CreateDiv( html );
        }
        return instance;
    }
})();
var a = new ProxySingletonCreateDiv( 'sven1' );
var b = new ProxySingletonCreateDiv( 'sven2' );
alert ( a === b );

JS中的单例模式

  • 使用命名空间
  • 使用闭包封装私有变量

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

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';
};

3. 策略模式

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

策略模式的目的就是将算法的使用与算法的实现分离开来。

var strategies = {
    "S": function( salary ){
        return salary * 4;
    },
    "A": function( salary ){
        return salary * 3;
    },
    "B": function( salary ){
        return salary * 2;
    }
};
var calculateBonus = function( level, salary ){
    return strategies[ level ]( salary );
};
console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000

多态在策略模式中的体现
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在Context 中,而是分布在各个策略对象中。Context 并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换Context 中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。

用策略模式重构表单校验

<html>
    <body>
        <form action="http:// xxx.com/register" id="registerForm" method="post">
            请输入用户名:<input type="text" name="userName"/ >
            请输入密码:<input type="text" name="password"/ >
            请输入手机号码:<input type="text" name="phoneNumber"/ >
            <button>提交</button>
        </form>
        <script>
        /***********************策略对象**************************/
        var strategies = {
            isNonEmpty: function( value, errorMsg ){
                if ( value === '' ){
                    return errorMsg;
                }
            },
            minLength: function( value, length, errorMsg ){
                if ( value.length < length ){
                    return errorMsg;
                }
            },
            isMobile: function( value, errorMsg ){
                if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
                    return errorMsg;
                }
            }
        };
        /***********************Validator 类**************************/
        var Validator = function(){
            this.cache = [];
        };
        Validator.prototype.add = function( dom, rules ){
            var self = this;
            for ( var i = 0, rule; rule = rules[ i++ ]; ){
                (function( rule ){
                    var strategyAry = rule.strategy.split( ':' );
                    var errorMsg = rule.errorMsg;
                    self.cache.push(function(){
                        var strategy = strategyAry.shift();
                        strategyAry.unshift( dom.value );
                        strategyAry.push( errorMsg );
                        return strategies[ strategy ].apply( dom, strategyAry );
                    });
                })( rule )
            }
        };
        Validator.prototype.start = function(){
            for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
                var errorMsg = validatorFunc();
                if ( errorMsg ){
                    return errorMsg;
                }
            }
        };
        /***********************客户调用代码**************************/
        var registerForm = document.getElementById( 'registerForm' );
        var validataFunc = function(){
            var validator = new Validator();
            validator.add( registerForm.userName, [{
                strategy: 'isNonEmpty',
                errorMsg: '用户名不能为空'
            }, {
                strategy: 'minLength:6',
                errorMsg: '用户名长度不能小于10 位'
            }]);
            validator.add( registerForm.password, [{
                strategy: 'minLength:6',
                errorMsg: '密码长度不能小于6 位'
            }]);
            validator.add( registerForm.phoneNumber, [{
                strategy: 'isMobile',
                errorMsg: '手机号码格式不正确'
            }]);
            var errorMsg = validator.start();
                return errorMsg;
            }
            registerForm.onsubmit = function(){
                var errorMsg = validataFunc();
                if ( errorMsg ){
                    alert ( errorMsg );
                    return false;
                }
            };
        </script>
    </body>
</html>

策略模式的优缺点

  • 优点:
  1. 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  2. 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy 中,使得它们易于切换,易于理解,易于扩展。
  3. 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  4. 在策略模式中利用组合和委托来让Context 拥有执行算法的能力,这也是继承的一种更轻 便的替代方案。
  • 缺点
  1. 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的 逻辑堆砌在Context 中要好。
  2. 要使用策略模式,必须了解所有的strategy,必须了解各个strategy 之间的不同点, 这样才能选择一个合适的strategy。此时strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。

4. 代理模式

为一个对象提供一个代用品或占位符,以便控制对它的访问。

例子说明: 在四月一个晴朗的早晨,小明遇见了他的百分百女孩,我们暂且称呼小明的女神为 A。两天之后,小明决定给A 送一束花来表白。刚好小明打听到A 和他有一个共同的朋 友B,于是内向的小明决定让B 来代替自己完成送花这件事情。

保护代理
代理B可以帮助A 过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B 处被拒绝掉。这种代理叫作保护代理。
用于控制不同权限的对象对目标对象的访问

虚拟代理
假设现实中的花价格不菲,导致在程序世界里,new Flower 也是一个代价昂贵的操作, 那么我们可以把new Flower 的操作交给代理B 去执行,代理B 会选择在A 心情好时再执行new Flower,这是代理模式的另一种形式,叫作虚拟代理。
虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
使用场景: 图片预加载

var myImage = (function(){
    var imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    return {
        setSrc: function( src ){
            imgNode.src = src;
        }
    }
})();
var proxyImage = (function(){
    var img = new Image;
    img.onload = function(){
        myImage.setSrc( this.src );
    }
    return {
        setSrc: function( src ){
            myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
            img.src = src;
        }
    }
})();
proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

代理的意义:

  • 遵循单一职责原则。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
  • 符合开放—封闭原则的。给img 节点设置src 和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。

代理和本体接口的一致性

  • 用户可以放心地请求代理,他只关心是否能得到想要的结果。
  • 在任何使用本体的地方都可以替换成使用代理。

虚拟代理合并HTTP请求

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;
};
mult( 2, 3 ); // 输出:6
mult( 2, 3, 4 ); // 输出:24

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 ); // 输出:24
proxyMult( 1, 2, 3, 4 ); // 输出:24
  • 缓存代理用于ajax异步请求数据
    我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已 经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。

  • 用高阶函数动态创建代理

/**************** 创建缓存代理的工厂 *****************/
var createProxyFactory = function( fn ){
    var cache = {};
        return function(){
            var args = Array.prototype.join.call( arguments, ',' );
            if ( args in cache ){
                return cache[ args ];
            }
        return cache[ args ] = fn.apply( this, arguments );
    }
};
var proxyMult = createProxyFactory( mult ),
  • 其他代理模式
  1. 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
  2. 远程代理:为一个对象在不同的地址空间提供局部代表,在Java 中,远程代理可以是另一个虚拟机中的对象。
  3. 保护代理:用于对象应该有不同访问权限的情况。
  4. 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
  5. 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程, 当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL (操作系统中的动态链接库)是其典型运用场景。

5. 迭代器模式

提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象 的内部表示。

应用举例: 根据不同的环境获取相应的上传对象

var getActiveUploadObj = function(){
    try{
        return new ActiveXObject( "TXFTNActiveX.FTNUpload" ); // IE 上传控件
    }catch(e){
        return false;
    }
};
var getFlashUploadObj = function(){
    if ( supportFlash() ){ // supportFlash 函数未提供
        var str = '<object type="application/x-shockwave-flash"></object>';
        return $( str ).appendTo( $('body') );
    }
    return false;
};
var getFormUpladObj = function(){
    var str = '<input name="file" type="file" class="ui-file"/>'; // 表单上传
    return $( str ).appendTo( $('body') );
};

var iteratorUploadObj = function(){
    for ( var i = 0, fn; fn = arguments[ i++ ]; ){
        var uploadObj = fn();
        if ( uploadObj !== false ){
            return uploadObj;
        }
    }
};
var uploadObj = iteratorUploadObj( getActiveUploadObj, getFlashUploadObj, getFormUpladObj );

6. 发布-订阅模式

又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。

var Event = (function(){
    var clientList = {},
        listen,
        trigger,
        remove;
        listen = function( key, fn ){
            if ( !clientList[ key ] ){
                clientList[ key ] = [];
            }
            clientList[ key ].push( fn );
        };
        trigger = function(){
            var key = Array.prototype.shift.call( arguments ),
            fns = clientList[ key ];
            if ( !fns || fns.length === 0 ){
                return false;
            }
            for( var i = 0, fn; fn = fns[ i++ ]; ){
                fn.apply( this, arguments );
            }
        };
        remove = function( key, fn ){
            var fns = clientList[ key ];
            if ( !fns ){
                return false;
            }
            if ( !fn ){
                fns && ( fns.length = 0 );
            }else{
                for ( var l = fns.length - 1; l >=0; l-- ){
                var _fn = fns[ l ];
                if ( _fn === fn ){
                    fns.splice( l, 1 );
                }
            }
        }
    };
    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})();
Event.listen( 'squareMeter88', function( price ){ // 小红订阅消息
    console.log( '价格= ' + price ); // 输出:'价格=2000000'
});
Event.trigger( 'squareMeter88', 2000000 ); // 售楼处发布消息

以上可能出现命名冲突的问题,可以通过给Event对象提供创建命名空间的功能。

先发布再订阅
为了满足这个需求,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还 没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ 的未读消息只会被重新阅读一次,所以刚才的操作我们只能进行一次。

优点

  • 时间上的解耦
  • 对象之间的解耦

缺点

  • 创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。
  • 虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。

7. 命令模式

命令(command)指的是一个执行某些特定事情的指令。

应用场景
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请 求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接 收者能够消除彼此之间的耦合关系。

<body>
    <div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
输入小球移动后的位置:<input id="pos"/>
    <button id="moveBtn">开始移动</button>
    <button id="cancelBtn">cancel</cancel> <!--增加取消按钮-->
</body>
<script>
    var ball = document.getElementById( 'ball' );
    var pos = document.getElementById( 'pos' );
    var moveBtn = document.getElementById( 'moveBtn' );
    var cancelBtn = document.getElementById( 'cancelBtn' );
    var MoveCommand = function( receiver, pos ){
        this.receiver = receiver;
        this.pos = pos;
        this.oldPos = null;
    };
    MoveCommand.prototype.execute = function(){
        this.receiver.start( 'left', this.pos, 1000, 'strongEaseOut' );
        this.oldPos = this.receiver.dom.getBoundingClientRect()[ this.receiver.propertyName ];
        // 记录小球开始移动前的位置
    };
    MoveCommand.prototype.undo = function(){
        this.receiver.start( 'left', this.oldPos, 1000, 'strongEaseOut' );
        // 回到小球移动前记录的位置
    };
    var moveCommand;
    moveBtn.onclick = function(){
        var animate = new Animate( ball );
        moveCommand = new MoveCommand( animate, pos.value );
        moveCommand.execute();
    };
    cancelBtn.onclick = function(){
        moveCommand.undo(); // 撤销命令
    };
</script>

命令模式的作用不仅是封装运算块,而且可以很方便地给命令对象增加撤销操作。

8. 组合模式

是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。

最大的优点在于可以一致地对待组合对象和基本对象.

应用实例: 扫描文件夹

/******************************* Folder ******************************/
var Folder = function( name ){
    this.name = name;
    this.files = [];
};
Folder.prototype.add = function( file ){
    this.files.push( file );
};
Folder.prototype.scan = function(){
    console.log( '开始扫描文件夹: ' + this.name );
    for ( var i = 0, file, files = this.files; file = files[ i++ ]; ){
        file.scan();
    }
};
/******************************* File ******************************/
var File = function( name ){
    this.name = name;
};
File.prototype.add = function(){
    throw new Error( '文件下面不能再添加文件' );
};
File.prototype.scan = function(){
    console.log( '开始扫描文件: ' + this.name );
};
var folder = new Folder( '学习资料' );
var folder1 = new Folder( 'JavaScript' );
var folder2 = new Folder ( 'jQuery' );
var file1 = new File( 'JavaScript 设计模式与开发实践' );
var file2 = new File( '精通jQuery' );
var file3 = new File( '重构与模式' )
folder1.add( file1 );
folder2.add( file2 );
folder.add( folder1 );
folder.add( folder2 );
folder.add( file3 );
var folder3 = new Folder( 'Nodejs' );
var file4 = new File( '深入浅出Node.js' );
folder3.add( file4 );
var file5 = new File( 'JavaScript 语言精髓与编程实践' );
folder.add( folder3 );
folder.add( file5 );

folder.scan();

注意

  • 组合模式不是父子关系,而是聚合
  • 对叶对象操作的一致性
  • 双向映射关系
  • 用职责链模式提高组合模式性能

应用场景

  • 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整 体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放-封闭原则。
  • 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别, 客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情, 这是组合模式最重要的能力。

9. 模板方法模式

是一种只需使用继承就可以实现的非常简单的模式。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

模板方法模式是一种严重依赖抽象类的设计模式。

var Beverage = function(){};
Beverage.prototype.boilWater = function(){
    console.log( '把水煮沸' );
};
Beverage.prototype.brew = function(){}; // 空方法,应该由子类重写
Beverage.prototype.pourInCup = function(){}; // 空方法,应该由子类重写
Beverage.prototype.addCondiments = function(){}; // 空方法,应该由子类重写
Beverage.prototype.init = function(){
    this.boilWater();
    this.brew();
    this.pourInCup();
    this.addCondiments();
};

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();

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();

可以通过“好莱坞原则”,不应继承也达到以上效果

允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”。

var Beverage = function( param ){
    var boilWater = function(){
        console.log( '把水煮沸' );
    };
    var brew = param.brew || function(){
        throw new Error( '必须传递brew 方法' );
    };
    var pourInCup = param.pourInCup || function(){
        throw new Error( '必须传递pourInCup 方法' );
    };
    var addCondiments = param.addCondiments || function(){
        throw new Error( '必须传递addCondiments 方法' );
    };
    var F = function(){};
    F.prototype.init = function(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    };
    return F;
};

var Coffee = Beverage({
    brew: function(){
        console.log( '用沸水冲泡咖啡' );
    },
    pourInCup: function(){
        console.log( '把咖啡倒进杯子' );
    },
    addCondiments: function(){
        console.log( '加糖和牛奶' );
    }
});

var Tea = Beverage({
    brew: function(){
        console.log( '用沸水浸泡茶叶' );
    },
    pourInCup: function(){
        console.log( '把茶倒进杯子' );
    },
    addCondiments: function(){
        console.log( '加柠檬' );
    }
});

var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();

模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。

10.享元模式

享元(flyweight)模式是一种用于性能优化的模式。
享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。
如何划分:

  • 内部状态存储于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

享元模式是一种用时间换空间的优化模式。

适用场景:

  • 一个程序中使用了大量的相似对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 对象的大多数状态都可以变为外部状态。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
    如:文件上传
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 );
    }
};

startUpload( 'plugin', [
    {
        fileName: '1.txt',
        fileSize: 1000
    },
    {
        fileName: '2.html',
        fileSize: 3000
    },
    {
        fileName: '3.txt',
        fileSize: 5000
    }
]);
startUpload( 'flash', [
    {
        fileName: '4.txt',
        fileSize: 1000
    },
    {
        fileName: '5.html',
        fileSize: 3000
    },
    {
        fileName: '6.txt',
        fileSize: 5000
    }
]);

11. 职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

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( '手机库存不足' );
    }
};

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 chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal );

chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );

chainOrder500.passRequest( 1, true, 500 ); // 输出:500 元定金预购,得到100 优惠券
chainOrder500.passRequest( 2, true, 500 ); // 输出:200 元定金预购,得到50 优惠券
chainOrder500.passRequest( 3, true, 500 ); // 输出:普通购买,无优惠券
chainOrder500.passRequest( 1, false, 0 ); // 输出:手机库存不足

用AOP 实现职责链

Function.prototype.after = function( fn ){
    var self = this;
    return function(){
        var ret = self.apply( this, arguments );
        if ( ret === 'nextSuccessor' ){
            return fn.apply( this, arguments );
        }
        return ret;
    }
};
var order = order500yuan.after( order200yuan ).after( orderNormal );
order( 1, true, 500 ); // 输出:500 元定金预购,得到100 优惠券
order( 2, true, 500 ); // 输出:200 元定金预购,得到50 优惠券
order( 1, false, 500 ); // 输出:普通购买,无优惠券

优点

  • 职责链模式的最大优点就是解耦了请求发送者和N个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发出的请求,所以你只需把请求传递给第一个节点即可。
  • 职责链模式还有一个优点,那就是可以手动指定起始节点,请求并不是非得从链中的第一个节点开始传递。

缺点

  • 不能保证某个请求一定会被链中的节点处理。所以需要在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
  • 使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能损耗。

12. 中介者模式

中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。

适用场景

  • 游戏中处理角色之前的关联逻辑
  • 购买商品显示库存是否不足
var goods = { // 手机库存
    "red|32G": 3,
    "red|16G": 0,
    "blue|32G": 1,
    "blue|16G": 6
};
var mediator = (function(){
    var colorSelect = document.getElementById( 'colorSelect' ),
        memorySelect = document.getElementById( 'memorySelect' ),
        numberInput = document.getElementById( 'numberInput' ),
        colorInfo = document.getElementById( 'colorInfo' ),
        memoryInfo = document.getElementById( 'memoryInfo' ),
        numberInfo = document.getElementById( 'numberInfo' ),
        nextBtn = document.getElementById( 'nextBtn' );
    return {
        changed: function( obj ){
            var color = colorSelect.value, // 颜色
                memory = memorySelect.value,// 内存
                number = numberInput.value, // 数量
                stock = goods[ color + '|' + memory ]; // 颜色和内存对应的手机库存数量
            if ( obj === colorSelect ){ // 如果改变的是选择颜色下拉框
                colorInfo.innerHTML = color;
            }else if ( obj === memorySelect ){
                memoryInfo.innerHTML = memory;
            }else if ( obj === numberInput ){
                numberInfo.innerHTML = number;
            }
            if ( !color ){
                nextBtn.disabled = true;
                nextBtn.innerHTML = '请选择手机颜色';
                return;
            }
            if ( !memory ){
                nextBtn.disabled = true;
                nextBtn.innerHTML = '请选择内存大小';
                return;
            }
            if ( ( ( number - 0 ) | 0 ) !== number - 0 ){ // 输入购买数量是否为正整数
                nextBtn.disabled = true;
                nextBtn.innerHTML = '请输入正确的购买数量';
                return;
            }
            nextBtn.disabled = false;
            nextBtn.innerHTML = '放入购物车';
        }
    }
})();

// 事件函数:
colorSelect.onchange = function(){
    mediator.changed( this );
};
memorySelect.onchange = function(){
    mediator.changed( this );
};
numberInput.oninput = function(){
    mediator.changed( this );
};

缺点 最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

13. 装饰者模式

给对象动态地增加职责

用AOP装饰函数

  • 数据统计上报
Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
    return ret;
}
};
var showLogin = function(){
    console.log( '打开登录浮层' );
}
var log = function(){
    console.log( '上报标签为: ' + this.getAttribute( 'tag' ) );
}
showLogin = showLogin.after( log ); // 打开登录浮层之后上报数据
document.getElementById( 'button' ).onclick = showLogin;
  • 用AOP动态改变函数的参数
Function.prototype.before = function( beforefn ){
    var __self = this;
    return function(){
        beforefn.apply( this, arguments ); // (1)
        return __self.apply( this, arguments ); // (2)
    }
}

var func = function( param ){
    console.log( param ); // 输出: {a: "a", b: "b"}
}
func = func.before( function( param ){
    param.b = 'b';
});
func( {a: 'a'} );

为解决CSRF给ajax请求加token

var getToken = function(){
    return 'Token';
}
ajax = ajax.before(function( type, url, param ){
    param.Token = getToken();
});
ajax( 'get', 'http:// xxx.com/userinfo', { name: 'sven' } );
  • 插件式的表单验证
Function.prototype.before = function( beforefn ){
    var __self = this;
    return function(){
        if ( beforefn.apply( this, arguments ) === false ){
            // beforefn 返回false 的情况直接return,不再执行后面的原函数
            return;
        }
        return __self.apply( this, arguments );
    }
}
var validata = function(){
    if ( username.value === '' ){
        alert ( '用户名不能为空' );
        return false;
    }
    if ( password.value === '' ){
        alert ( '密码不能为空' );
        return false;
    }
}
var formSubmit = function(){
    var param = {
        username: username.value,
        password: password.value
    }
    ajax( 'http:// xxx.com/login', param );
}
formSubmit = formSubmit.before( validata );
submitBtn.onclick = function(){
    formSubmit();
}

14. 状态模式

状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

var OffLightState = function( light ){
    this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){
    console.log( '弱光' ); // offLightState 对应的行为
    this.light.setState( this.light.weakLightState ); // 切换状态到weakLightState
};
// WeakLightState:
var WeakLightState = function( light ){
    this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function(){
    console.log( '强光' ); // weakLightState 对应的行为
    this.light.setState( this.light.strongLightState ); // 切换状态到strongLightState
};
// StrongLightState:
var StrongLightState = function( light ){
    this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function(){
    console.log( '关灯' ); // strongLightState 对应的行为
    this.light.setState( this.light.offLightState ); // 切换状态到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 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。

缺点

  • 会在系统中定义许多状态类
  • 由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。

状态机 状态模式是状态机的实现之一

var Light = function(){
    this.currState = FSM.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.currState.buttonWasPressed.call( self ); // 把请求委托给FSM 状态机
}
};
var FSM = {
    off: {
        buttonWasPressed: function(){
            console.log( '关灯' );
            this.button.innerHTML = '下一次按我是开灯';
            this.currState = FSM.on;
        }
    },
    on: {
        buttonWasPressed: function(){
            console.log( '开灯' );
            this.button.innerHTML = '下一次按我是关灯';
            this.currState = FSM.off;
        }
    }
};
var light = new Light();
light.init();

15. 适配器模式

是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。