《javascript设计模式与开发实战》-设计模式(14种)篇

166 阅读33分钟

前言

最近无聊看了看书,总结了一下!
阅读本章需要先了解基本的JavaScript知识,包括但不限于闭包、原型、回调函数等

单例模式

概念

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

情景模拟

创建 CreateDiv 单例类,它的作用是负责在页面中创建一个唯一的 div 节点

代码对比示例

//代理方式实现单例模式
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);
    };
    // 接下来引入代理类 proxySingletonCreateDiv:
    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');
    console.log(a, b); //html: "sven1" , html: "sven1"

惰性单例

//惰性单例指的是在需要的时候才创建对象实例,这里我们同样采用上面的场景例子好做对比(不和书中一样)

//创建管理单例函数
   var getSingle = function (fn) {
        var result;
        return function () {
            return result || (result = fn.apply(this, arguments)); //arguments作为fn函数的参数
        }
    };
//创建节点
    var createDiv = function (html) {
        var div = document.createElement('div');
        div.innerHTML = html;
        document.body.appendChild(div);
        return div
    };
    var createSingleDiv = getSingle(createDiv);
    
    // 两秒后 需要的时候我们才创建对象实例
    setTimeout(function(){
        var div1=createSingleDiv('div1')
        var div2=createSingleDiv('div2')
        console.log(div1,div2) //<div>div1</div> , <div>div1</div>
        // div1.innerHTML="访问"
    },2000)

note

惰性单例模式这里使用了apply()方法不了解戳apply()

策略模式

概念:

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

情景模拟

很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S 的人年终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

代码对比示例

通常写法

var calculateBonus = function (performanceLevel, salary) {
        if (performanceLevel === 'S') {
            return salary * 4;
        }
        if (performanceLevel === 'A') {
            return salary * 3;
        }
        if (performanceLevel === 'B') {
            return salary * 2;
        }
    };
    calculateBonus('B', 20000); // 输出:40000 
    calculateBonus('S', 6000); // 输出:24000

模式写法

    //在js中函数也是对象,那么我们可以非常清晰的去通过key->function的方式实现,如下:
    //算法的实现
     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

note

通过对策略模式的大概熟悉,可以联想下其他使用场景:比如在诸多if实现不同结果时、在jsx中通过不同条件渲染不同样式类甚至是不同组件时...

代理模式

概念

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用

情景模拟

小明决定给 A 送一束花来表白。刚好小明打听到 A 和他有一个共同的朋友 B,于是内向的小明决定让 B 来代替自己完成送花这件事情。
当 A 在心情好的时候收到花,小明表白成功的几率有60%,而当 A 在心情差的时候收到花,小明表白的成功率无限趋近于 0。
小明跟 A 刚刚认识两天,还无法辨别 A 什么时候心情好。B 却很了解 A,所以小明只管把花交给 B,B 会监听 A 的心情变化,然后选择 A 心情好的时候把花转交给 A。
代码如下:

 var Flower = function () { }; // 花
    var xiaoming = { // 小明送花
        sendFlower: function (target) {
            var flower = new Flower();
            target.receiveFlower(flower);
        }
    };
    var B = {  //代理b
        receiveFlower: function (flower) {
            A.listenGoodMood(function () { //  监听 A 的好心情
                A.receiveFlower(flower); // 代理送花
            });
        }
    };
    var A = {
        receiveFlower: function (flower) {// 收花
            console.log('收到花 ', flower);
        },
        listenGoodMood: function (fn) {
            setTimeout(function () { // 假设 10 秒之后 A 的心情变好
                fn();
            }, 2000);
        }
    };
    
    xiaoming.sendFlower(B);

虽然这只是个虚拟的例子,但我们可以从中找到两种代理模式的身影。
1、代理 B 可以帮助 A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 B处被拒绝掉。这种代理叫作保护代理。
2、另外,假设现实中的花价格不菲,导致在程序世界里,new Flower 也是一个代价昂贵的操作,那么我们可以把 new Flower 的操作交给代理 B 去执行,代理 B 会选择在 A 心情好时再执行 new Flower,这是代理模式的另一种形式,叫作虚拟代理。

代码对比示例

通过虚拟代理实现图片懒加载

//引入代理对象 proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中
// 将出现一张占位图 loading.gif, 来提示用户图片正在加载。
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('./img/loading.gif');//设置占位图
                img.src = src;
            }
        }
    })();
    proxyImage.setSrc('https://***/2f8d10fd50154518a08321f065862042_1f49c8c576a7a165796cd4b9f3d32f0f_127055.jpg');

除了上面的两种代理,我们再看看缓存代理例子:

/** 所谓缓存代理就是可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参
数跟之前一致,则可以直接返回前面存储的运算结果。*/
//一个简单的求乘积的程序如下:
var mult = function () {
        console.log('开始计算乘积');
        var a = 1;
        for (var i = 0, l = arguments.length; i < l; i++) {
            a = a * arguments[i];
        }
        return a;
    };
    //代理对象proxyMult
    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);
        }
    })();
    console.log(proxyMult(1, 2, 3, 4)); // 输出:24
    console.log(proxyMult(1, 2, 3, 4)); // 输出:24
  //书中还有用高阶函数动态创建代理(比如为乘法、加法、减法等创建缓存代理),这里不做阐述
    

//缓存代理还可用于ajax异步请求数据比如:

我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,
这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。

note

代理模式包括许多小分类,在 JavaScript 开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。

迭代器模式

概念

迭代器模式无非就是循环访问聚合对象中的各个元素。比如 jQuery 中的$.each 函数,其中回 调函数中的参数 i 为当前索引,n 为当前元素,代码如下:

    $.each([1, 2, 3], function (i, n) {
        console.log('当前下标为: ' + i);
        console.log('当前值为:' + n);
    });

情景模拟

判断 2 个数组里元素的值是否完全相等。

代码对比示例

内部迭代器

/**
内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅
仅是一次初始调用,但这也刚好是内部迭代器的缺点。
*/
 var each = function( ary, callback ){ 
    for ( var i = 0, l = ary.length; i < l; i++ ){ 
    callback.call( ary[i], i, ary[ i ] ); // 把下标和元素当作参数传给 callback 函数
    } 
    };
    var compare = function( ary1, ary2 ){ 
     if ( ary1.length !== ary2.length ){ 
     throw new Error ( 'ary1 和 ary2 不相等' ); 
    } 
    each( ary1, function( i, n ){ 
     if ( n !== ary2[ i ] ){ 
     throw new Error ( 'ary1 和 ary2 不相等' ); 
    } 
    }); 
     console.log ( 'ary1 和 ary2 相等' ); 
    }; 
    compare( [ 1, 2, 3 ], [ 1, 2, 3, 4 ] ); // throw new Error ( 'ary1 和 ary2 不相等' );

外部迭代器

/**
外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。内部迭代
器和外部迭代器在实际生产中没有优劣之分,究竟使用哪个要根据需求场景而定。
*/
 var Iterator = function (obj) {
        var current = 0;
        var next = function () {
            current += 1;
        };
        var isDone = function () {
            return current >= obj.length;
        };
        var getCurrItem = function () {
            return obj[current];
        };
        return {
            next: next,
            isDone: isDone,
            getCurrItem: getCurrItem
        }
    };
    // 再看看如何改写 compare 函数:
    var compare = function (iterator1, iterator2) {
        while (!iterator1.isDone() && !iterator2.isDone()) {
            if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
                throw new Error('iterator1 和 iterator2 不相等');
            }
            iterator1.next();
            iterator2.next();
        }
        alert('iterator1 和 iterator2 相等');
    }
    var iterator1 = Iterator([1, 2, 3]);
    var iterator2 = Iterator([1, 2, 3]);
    compare(iterator1, iterator2); // 输出:iterator1 和 iterator2 相等
    
    /**这里书中原文有一个问题,并没有对数组长度进行判断,我们对迭代器Iterator作一个修改,
    同时也恰恰说明了满足更多变的需求。
    */
    
    var Iterator = function (obj) {
        var current = 0;
        var next = function () {
            current += 1;
        };
        var isDone = function () { //循环完成
            console.log(current)
            return current >= obj.length;
        };
        var getCurrItem = function () {
            return obj[current];
        };
         
        var getLength = function () {  //注:这里我们自己补加了一个获取长度的迭代方法
            return obj.length
        }
        return {
            next: next,
            isDone: isDone,
            getCurrItem: getCurrItem,
            getLength: getLength
        }
    };
    
    var compare = function (iterator1, iterator2) {
        // 先判断长度
        if (iterator1.getLength() !== iterator2.getLength()) {
            throw new Error('iterator1 和 iterator2 不相等');
        }
        while (!iterator1.isDone() && !iterator2.isDone()) {
            if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
                throw new Error('iterator1 和 iterator2 不相等');
            }
            iterator1.next();
            iterator2.next();
        }
        console.log('iterator1 和 iterator2 相等');
    }
    var iterator1 = Iterator([1, 2, 3, 4]);
    var iterator2 = Iterator([1, 2, 3]);
    compare(iterator1, iterator2); // 输出:iterator1 和 iterator2 不相等
    

note

除了上述提到的内部迭代器、外部迭代器、还有倒序迭代器、中止迭代器。也都是字面意思。
迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前的绝大部分语言都内置了迭代器,js中可以参考Symbol.iterator。

下面是一个迭代器,实现对象可被for...of 循环使用的示例:

var myIterable = { a: 1, b: 2 }
    Object.prototype[Symbol.iterator] = function* () {
        var arr = Reflect.ownKeys(this),
            index = 0;
        while (index < arr.length)
            yield this[arr[index++]];
    };
    for (const element of myIterable) {
        console.log(element); //输出:1 2 
    }

发布订阅模式

概念

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知

情景模拟

小明、小红、小强和小龙想买房子,把电话号码留在了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们。 (发送短信通知就是一个典型的发布—订阅模式)

代码示例

通用实现

  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 时带上的参数
            }
        }
    };
    //添加删除订阅事件
    // event.remove = function (key, fn) {
    //     var fns = this.clientList[key];
    //     if (!fns) { // 如果 key 对应的消息没有被人订阅,则直接返回
    //         return false;
    //     }
    //     if (!fn) { // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
    //         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); // 删除订阅者的回调函数
    //             }
    //         }
    //     }
    // };
    
    //再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能:
    var installEvent = function (obj) {
        for (var i in event) {
            obj[i] = event[i];
        }
    };
    //再来测试一番,我们给售楼处对象 salesOffices 动态增加发布—订阅功能:
    var salesOffices = {};
    installEvent(salesOffices);
    salesOffices.listen('squareMeter88', function (price) { // 小明订阅消息
        console.log('价格= ' + price);
    });
    salesOffices.listen('squareMeter100', function (price) { // 小红订阅消息
        console.log('价格= ' + price);
    });

    salesOffices.trigger('squareMeter88', 2000000); //  输出:2000000
    salesOffices.trigger('squareMeter100', 3000000); // 输出:3000000
    // salesOffices.remove('squareMeter88'); // 删除小明的订阅
    // salesOffices.trigger('squareMeter88', 2000000); // 输出:空
    // salesOffices.trigger('squareMeter100', 3000000); // 输出:3000000
    
    //ps:可尝试删除订阅事件

全局的发布-订阅对象

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

网站登录场景代码实现:

var login = {
        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 时带上的参数
            }
        }
    };
    
    // $.ajax('http:// xxx.com?login', function (data) { // 登录成功
    setTimeout(() => { //我们用定时器模拟异步登录场景
        login.trigger('loginSucc', 'data'); // 发布登录成功的消息
    }, 1000);
    // });
    
    // 各模块监听登录成功的消息:
    var header = (function () { // header 模块
        login.listen('loginSucc', function (data) {
            header.setAvatar(data.avatar);
        });
        return {
            setAvatar: function (data) {
                console.log('设置 header 模块的头像');
            }
        }
    })();

    //通过对登录的监听 处理一系列模块功能
    var nav = (function () { // nav 模块
        login.listen('loginSucc', function (data) {
            nav.setAvatar(data.avatar);
        });
        return {
            setAvatar: function (avatar) {
                console.log('设置 nav 模块的头像');
            }
        }
    })();
    
   //输出 设置 header 模块的头像  设置 nav 模块的头像

注意:上面无论是小明买房还是登录都是通过listen方法先订阅-而后发布的。
也许我们还需要一个方案,使得我们的发布—订阅对象拥有先发布后订阅的能力,并且全局发布订阅对象我们需要避免命名冲突。 代码实现:

 var Event = (function () {
        var global = this,
            Event,
            _default = 'default';
        Event = function () {
            var _listen,
                _trigger,
                _remove,
                _slice = Array.prototype.slice,
                _shift = Array.prototype.shift,
                _unshift = Array.prototype.unshift,
                namespaceCache = {},
                _create,
                find,
                each = function (ary, fn) {
                    var ret;
                    for (var i = 0, l = ary.length; i < l; i++) {
                        var n = ary[i];
                        ret = fn.call(n, i, n);
                    }
                    return ret;
                };
            _listen = function (key, fn, cache) {
                if (!cache[key]) {
                    cache[key] = [];
                }
                cache[key].push(fn);
            };
            _remove = function (key, cache, fn) {
                if (cache[key]) {
                    if (fn) {
                        for (var i = cache[key].length; i >= 0; i--) {
                            if (cache[key][i] === fn) {
                                cache[key].splice(i, 1);
                            }
                        }
                    } else {
                        cache[key] = [];
                    }
                }
            };
            _trigger = function () {
                var cache = _shift.call(arguments),
                    key = _shift.call(arguments),
                    args = arguments,
                    _self = this,
                    ret,
                    stack = cache[key];
                if (!stack || !stack.length) {
                    return;
                }
                return each(stack, function () {
                    return this.apply(_self, args);
                });
            };
            _create = function (namespace) {
                var namespace = namespace || _default;
                var cache = {},
                    offlineStack = [], // 离线事件 
                    ret = {
                        listen: function (key, fn, last) {
                            _listen(key, fn, cache);
                            if (offlineStack === null) {
                                return;
                            }
                            if (last === 'last') {
                                offlineStack.length && offlineStack.pop()();
                            } else {
                                each(offlineStack, function () {
                                    this();
                                });
                            }
                            offlineStack = null;
                        },
                        one: function (key, fn, last) {
                            _remove(key, cache);
                            this.listen(key, fn, last);
                        },
                        remove: function (key, fn) {
                            _remove(key, cache, fn);
                        },
                        trigger: function () {
                            var fn,
                                args,
                                _self = this;
                            _unshift.call(arguments, cache);
                            args = arguments;
                            fn = function () {
                                return _trigger.apply(_self, args);
                            };
                            if (offlineStack) {
                                return offlineStack.push(fn);
                            }
                            return fn();
                        }
                    };
                return namespace ?
                    (namespaceCache[namespace] ? namespaceCache[namespace] :
                        namespaceCache[namespace] = ret)
                    : ret;
            };
            return {
                create: _create,
                one: function (key, fn, last) {
                    var event = this.create();
                    event.one(key, fn, last);
                },
                remove: function (key, fn) {
                    var event = this.create();
                    event.remove(key, fn);
                },
                listen: function (key, fn, last) {
                    var event = this.create();
                    event.listen(key, fn, last);
                },
                trigger: function () {
                    var event = this.create();
                    event.trigger.apply(this, arguments);
                }
            };
        }();
        return Event;
    })();
    Event.trigger('click', 1);
    Event.listen('click', function (a) {
        console.log(a); // 输出:1 
    });
    /************** 使用命名空间 ********************/
    Event.create('namespace1').listen('click', function (a) {
        console.log(a); // 输出:1 
    });
    Event.create('namespace1').trigger('click', 1);

note

从通用实现到最后完善都是一个补丁的过程,可以对比来看。想要搞懂这几段需要首先了解arguments和Function.prototype.apply。

命令模式

概念

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

场景

在打斗游戏中,命令模式可以用来实现播放录像功能。我们把用户在键盘的输入都封装成命令,执行过的命令将被存放到堆栈中。播放录像的时候只需要从头开始依次执行这些命令便可,代码如下:

代码实现

var Ryu = {
        attack: function () {
            console.log('攻击');
        },
        defense: function () {
            console.log('防御');
        },
        jump: function () {
            console.log('跳跃');
        },
        crouch: function () {
            console.log('蹲下');
        }
    };

    var makeCommand = function (receiver, state) { // 创建命令
        return function () {
            receiver[state]();
        }
    };

    var commands = {
        "119": "jump", // W 
        "115": "crouch", // S 
        "97": "defense", // A 
        "100": "attack" // D 
    };
    var commandStack = []; // 保存命令的堆栈
    document.onkeypress = function (ev) {
        var keyCode = ev.keyCode,
            command = makeCommand(Ryu, commands[keyCode]);
        if (command) {
            command(); // 执行命令
            commandStack.push(command); // 将刚刚执行过的命令保存进堆栈 (把子命令添加进宏命令对象)
        }
    };

    document.getElementById('replay').onclick = function () { // 点击播放录像
        var command;
        while (command = commandStack.shift()) { // 从堆栈里依次取出命令并执行(撤消和重做)
            command();
        }
    };

命令模式中通常包含撤消和重做、宏命令,在此例子中都有体现。

  • 示例二
    或许上面的例子有点复杂,我们再看一个比较纯粹的宏命令示例:
var closeDoorCommand = {
       execute: function () {
           console.log('关门');
       }
   };
   var openPcCommand = {
       execute: function () {
           console.log('开电脑');
       }
   };
   var openQQCommand = {
       execute: function () {
           console.log('登录 QQ');
       }
   };
   
   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 macroCommand = MacroCommand();
   macroCommand.add(closeDoorCommand);
   macroCommand.add(openPcCommand);
   macroCommand.add(openQQCommand);
   macroCommand.execute();

通过观察这段代码,我们很容易发现,宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树,如图:

image.png

note

  • 命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些 特定事情的指令。
  • JavaScript 可以用高阶函数非常方便地实现命令模式。命令模式在 JavaScript 语言中是一种隐形的模式。

组合模式

概念

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。

场景

上面【命令模式】中提到的万能遥控器,包含了关门、开电脑、登录 QQ 这 3 个命令。现在我们需要一个“超级万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能:

  • 打开空调
  • 打开电视和音响
  • 关门、开电脑、登录 QQ

代码示例

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); 
    //输出 打开空调 打开电视 打开音响 关门 开电脑 登录 QQ

从这个例子中可以看到,基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。 如图:

image.png
在树最终被构造完成之后,让整颗树最终运转起来的步骤非常简单,只需要调用最上层对象的 execute 方法。每当对最上层的对象进行一次请求时,实际上是在对整个树进行深度优先的搜索,而创建组合对象的程序员并不关心这些内在的细节,往这棵树里面添加一些新的节点对象是非常容易的事情。

  • 示例二
    扫描文件夹
 /******************************* 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);
    };
    // 接下来创建一些文件夹和文件对象, 并且让它们组合成一棵树,这棵树就是我们 F 盘里的
    // 现有文件目录结构:
    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();

我们改变了文件夹的结构,增加了新的文件对象,却不用修改任何一句原有的代码,这是符合开放-封闭原则的。
运用了组合模式之后,扫描整个文件夹的操作也是轻而易举的,我们只需要操作树的最顶端对象:folder.scan();输出如下:

image.png

note

组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。 以下情况下适用Composite模式:

  • 你想表示对象的部分-整体层次结构
  • 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

模板方法模式

概念

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

场景

现在我们需要先泡一杯咖啡再泡一杯茶,两者流程如下:

image.png

代码示例

经过思考和比较,我们发现咖啡和茶的冲泡过程是大同小异的。现在我们用代码实现:

//创建模板方法
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();

通过代码我们可以很清晰的看到,我们定义了一个公开定义了执行它的模板方法-Beverage,并且根据冲泡过程中的一些差异,重写了差异部分的方法实现。

note

模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放-封闭原则的。

享元模式

概念

享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少相似对象的重复创建,尝试重用(共享)现有的同类对象,以减少内存占用和提高性能。

  • 内部状态存储在享元内部,不会随环境的改变而有所不同,是可以共享的。
  • 外部状态是不可以共享的,它随环境的改变而改变的。

场景

假设有个内衣工厂,目前的产品有 50 种男式内衣和 50 种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50 个男模特和 50 个女模特,然后让他们每人分别穿上一件内衣来拍照。

代码对比示例

不使用享元模式的情况下,在程序里也许会这样写:

var Model = function (sex, underwear) {
        this.sex = sex;
        this.underwear = underwear;
    };
    Model.prototype.takePhoto = function () {
        console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
    };
    for (var i = 1; i <= 50; i++) {
        var maleModel = new Model('male', 'underwear' + i);
        maleModel.takePhoto();
    };
    for (var j = 1; j <= 50; j++) {
        var femaleModel = new Model('female', 'underwear' + j);
        femaleModel.takePhoto();
    };

如何优化?
现在来改写一下代码,既然只需要区别男女模特,只需要一男一女两个模特分别穿上50种内衣拍照就行了,那我们先把 underwear 参数从构造函数中移除,构造函数只接收 sex 参数:

var Model = function (sex) {
        this.sex = sex;
    };
    Model.prototype.takePhoto = function () {
        console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
    };
    // 分别创建一个男模特对象和一个女模特对象:
    var maleModel = new Model('male'),
        femaleModel = new Model('female');
    // 给男模特依次穿上所有的男装,并进行拍照:
    for (var i = 1; i <= 50; i++) {
        maleModel.underwear = 'underwear' + i;
        maleModel.takePhoto();
    };
    // 同样,给女模特依次穿上所有的女装,并进行拍照:
    for (var j = 1; j <= 50; j++) {
        femaleModel.underwear = 'underwear' + j;
        femaleModel.takePhoto();
    };

可以看到,改进之后的代码,只需要两个对象便完成了同样的功能。

note

享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。

职责链模式

概念

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

image.png

场景

假设我们负责一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定 后(订单已在此时生成),现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买后:

  • 已经支付过 500 元定金的用户会收到 100 元的商城优惠券
  • 200 元定金的用户可以收到 50 元的优惠券
  • 没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

代码对比示例

下面我们把这个流程写成代码:

 var order = function (orderType, pay, stock) {
        if (orderType === 1) { // 500 元定金购买模式
            if (pay === true) { // 已支付定金
                console.log('500 元定金预购, 得到 100 优惠券');
            } else { // 未支付定金,降级到普通购买模式
                if (stock > 0) { // 用于普通购买的手机还有库存
                    console.log('普通购买, 无优惠券');
                } else {
                    console.log('手机库存不足');
                }
            }
        }
        else if (orderType === 2) { // 200 元定金购买模式
            if (pay === true) {
                console.log('200 元定金预购, 得到 50 优惠券');
            } else {
                if (stock > 0) {
                    console.log('普通购买, 无优惠券');
                } else {
                    console.log('手机库存不足');
                }
            }
        }
        else if (orderType === 3) {
            if (stock > 0) {
                console.log('普通购买, 无优惠券');
            } else {
                console.log('手机库存不足');
            }
        }
    };
    order(1, true, 500); // 输出: 500 元定金预购, 得到 100 优惠券

虽然我们得到了意料中的运行结果,但这远远算不上一段值得夸奖的代码。order 函数不仅巨大到难以阅读,而且需要经常进行修改。现在我们采用职责链模式重构这段代码:

/**  1、先把 500 元订单、200 元订单以及普通购买分成 3个函数。
     2、接下来把 orderType、pay、stock 这 3 个字段当作参数传递给 500 元订单函数,如果该函数不
符合处理条件,则把这个请求传递给后面的 200 元订单函数,如果 200 元订单函数依然不能处理
该请求,则继续传递请求给普通购买函数,代码如下:
     */
      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;
    };
    // 现在我们把 3 个订单函数分别包装成职责链的节点:
    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); // 输出:手机库存不足

    //假如某天网站运营人员又想出了支持 300 元定金购买,那我们就在该链中增加一个节点即可:
    // var order300 = function () {
    //     // 具体实现略 
    // };
    // chainOrder300 = new Chain(order300);
    // chainOrder500.setNextSuccessor(chainOrder300);
    // chainOrder300.setNextSuccessor(chainOrder200);

异步情况下职责链

 var fn1 = new Chain(function () {
        console.log(1);
        return 'nextSuccessor';
    });
    var fn2 = new Chain(function () {
        console.log(2);
        var self = this;
        setTimeout(function () {
            self.next();
        }, 1000);
    });
    var fn3 = new Chain(function () {
        console.log(3);
    });
    fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
    fn1.passRequest();

note

无论是作用域链、原型链,还是 DOM 节点中的事件冒泡,我们都能从中找到职责链模式的影子。职责链模式还可以和组合模式结合在一起,用来连接部件和父部件,或是提高组合对象的效率。

中介者模式

概念

用一个中介对象来封装一系列对象的交互,从而把一批原来可能是交互关系复杂的对象转换成一组松散耦合的中间对象,以有利于维护和修改。

场景

大家可能都还记得泡泡堂游戏,现在我们来一起回顾这个游戏,双方队友开始炸泡泡,如果某一方队友全部死亡,则判定这局游戏失败!

代码对比实例

一般情况下代码可能会这么写:

//先写构造函数 Player,使每个玩家对象都增加一些属性,分别是队友列表、敌人列表 、玩家当前状态、角色名字以及玩家所在的队伍颜色:
    function Player(name, teamColor) {
        this.partners = []; // 队友列表
        this.enemies = []; // 敌人列表
        this.state = 'live'; // 玩家状态
        this.name = name; // 角色名字
        this.teamColor = teamColor; // 队伍颜色
    };
     Player.prototype.win = function () { // 玩家团队胜利
        console.log('winner: ' + this.name);
    };
     Player.prototype.lose = function () { // 玩家团队失败
        console.log('loser: ' + this.name);
    };
    /**    玩家死亡的方法要稍微复杂一点,我们需要在每个玩家死亡的时候,都遍历其他队友
     的生存状况,如果队友全部死亡,则这局游戏失败,同时敌人队伍的所有玩家都取得胜利,代
     码如下:*/
    Player.prototype.die = function () { // 玩家死亡
        var all_dead = true;
        this.state = 'dead'; // 设置玩家状态为死亡
        for (var i = 0, partner; partner = this.partners[i++];) { // 遍历队友列表
            if (partner.state !== 'dead') { // 如果还有一个队友没有死亡,则游戏还未失败
                all_dead = false;
                break;
            }
        }
        if (all_dead === true) { // 如果队友全部死亡
            this.lose(); // 通知自己游戏失败
            for (var i = 0, partner; partner = this.partners[i++];) { // 通知所有队友玩家游戏失败
                partner.lose();
            }
            for (var i = 0, enemy; enemy = this.enemies[i++];) { // 通知所有敌人游戏胜利
                enemy.win();
            }
        }
    };
     // 最后定义一个工厂来创建玩家:
    var playerFactory = function (name, teamColor) {
        var newPlayer = new Player(name, teamColor); // 创建新玩家
        for (var i = 0, player; player = players[i++];) { // 通知所有的玩家,有新角色加入
            if (player.teamColor === newPlayer.teamColor) { // 如果是同一队的玩家
                player.partners.push(newPlayer); // 相互添加到队友列表
                newPlayer.partners.push(player);
            } else {
                player.enemies.push(newPlayer); // 相互添加到敌人列表
                newPlayer.enemies.push(player);
            }
        }
        players.push(newPlayer);
        return newPlayer;
    };
     // 现在来感受一下, 用这段代码创建 8 个玩家:
    //红队:
    var player1 = playerFactory('皮蛋', 'red'),
        player2 = playerFactory('小乖', 'red'),
        player3 = playerFactory('宝宝', 'red'),
        player4 = playerFactory('小强', 'red');
    //蓝队:
    var player5 = playerFactory('黑妞', 'blue'),
        player6 = playerFactory('葱头', 'blue'),
        player7 = playerFactory('胖墩', 'blue'),
        player8 = playerFactory('海盗', 'blue');
    // 让红队玩家全部死亡:
    player1.die();
    player2.die();
    player4.die();
    player3.die();
    

image.png
使用中介者模式改造
现在我们已经可以随意地为游戏增加玩家或者队伍,但问题是,每个玩家和其他玩家都是紧 紧耦合在一起的。在此段代码中,每个玩家对象都有两个属性,this.partners 和 this.enemies, 用来保存其他玩家对象的引用。当每个对象的状态发生改变,比如角色移动、吃到道具或者死亡 时,都必须要显式地遍历通知其他对象。
现在我们开始用中介者模式来改造上面的泡泡堂游戏, 改造后的玩家对象和中介者的关系 如图:

image.png

/**
     * 首先仍然是定义 Player 构造函数和 player 对象的原型方法,在 player 对象的这些原型方法
中,不再负责具体的执行逻辑,而是把操作转交给中介者对象,我们把中介者对象命名为
playerDirector:
     */
    function Player(name, teamColor) {
        this.name = name; // 角色名字
        this.teamColor = teamColor; // 队伍颜色 
        this.state = 'alive'; // 玩家生存状态
    };
    Player.prototype.win = function () {
        console.log(this.name + ' won ');
    };
    Player.prototype.lose = function () {
        console.log(this.name + ' lost');
    };
    /*******************玩家死亡*****************/
    Player.prototype.die = function () {
        this.state = 'dead';
        playerDirector.reciveMessage('playerDead', this); // 给中介者发送消息,玩家死亡
    };
    /*******************移除玩家*****************/
    Player.prototype.remove = function () {
        playerDirector.reciveMessage('removePlayer', this); // 给中介者发送消息,移除一个玩家
    };
    /*******************玩家换队*****************/
    Player.prototype.changeTeam = function (color) {
        playerDirector.reciveMessage('changeTeam', this, color); // 给中介者发送消息,玩家换队
    };
    // 再继续改写之前创建玩家对象的工厂函数,可以看到,因为工厂函数里不再需要给创建的玩家对象设置队友和敌人,这个工厂函数几乎失去了工厂的意义:
    var playerFactory = function (name, teamColor) {
        var newPlayer = new Player(name, teamColor); // 创造一个新的玩家对象
        playerDirector.reciveMessage('addPlayer', newPlayer); // 给中介者发送消息,新增玩家
        return newPlayer;
    };

     /*******************添加中介者*****************/
    var playerDirector = (function () {
        var players = {}, // 保存所有玩家
            operations = {}; // 中介者可以执行的操作
        /****************新增一个玩家***************************/
        operations.addPlayer = function (player) {
            var teamColor = player.teamColor; // 玩家的队伍颜色
            players[teamColor] = players[teamColor] || []; // 如果该颜色的玩家还没有成立队伍,则
            // 新成立一个队伍
            players[teamColor].push(player); // 添加玩家进队伍
        };
        /****************移除一个玩家***************************/
        operations.removePlayer = function (player) {
            var teamColor = player.teamColor, // 玩家的队伍颜色
                teamPlayers = players[teamColor] || []; // 该队伍所有成员
            for (var i = teamPlayers.length - 1; i >= 0; i--) { // 遍历删除
                if (teamPlayers[i] === player) {
                    teamPlayers.splice(i, 1);
                }
            }
        };
        /****************玩家换队***************************/
        operations.changeTeam = function (player, newTeamColor) { // 玩家换队
            operations.removePlayer(player); // 从原队伍中删除
            player.teamColor = newTeamColor; // 改变队伍颜色
            operations.addPlayer(player); // 增加到新队伍中
        };
        operations.playerDead = function (player) { // 玩家死亡
            var teamColor = player.teamColor,
                teamPlayers = players[teamColor]; // 玩家所在队伍
            var all_dead = true;
            for (var i = 0, player; player = teamPlayers[i++];) {
                if (player.state !== 'dead') {
                    all_dead = false;
                    break;
                }
            }
            if (all_dead === true) { // 全部死亡
                for (var i = 0, player; player = teamPlayers[i++];) {
                    player.lose(); // 本队所有玩家 lose 
                }
                for (var color in players) {
                    if (color !== teamColor) {
                        var teamPlayers = players[color]; // 其他队伍的玩家
                        for (var i = 0, player; player = teamPlayers[i++];) {
                            player.win(); // 其他队伍所有玩家 win 
                        }
                    }
                }
            }
        };
        var reciveMessage = function () {
            var message = Array.prototype.shift.call(arguments); // arguments 的第一个参数为消息名称
            operations[message].apply(this, arguments);
        };
        return {
            reciveMessage: reciveMessage
        }
    })();
    
    // 红队:
    var player1 = playerFactory('皮蛋', 'red'),
        player2 = playerFactory('小乖', 'red'),
        player3 = playerFactory('宝宝', 'red'),
        player4 = playerFactory('小强', 'red');
    // 蓝队:
    var player5 = playerFactory('黑妞', 'blue'),
        player6 = playerFactory('葱头', 'blue'),
        player7 = playerFactory('胖墩', 'blue'),
        player8 = playerFactory('海盗', 'blue');
    player1.die();
    player2.die();
    player3.die();
    player4.die();

可以看到,除了中介者本身,没有一个玩家知道其他任何玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其他玩家,而只需要给中介者发送一个消息。(代码看下来,相信大部分人看到playerDirector函数时都相对费时,这也正是这种模式的缺点)

note

优点: 中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系(是一种迪米特法则的实现)。
缺点:系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性。

装饰者模式

概念

装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

场景

我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们一般都会先保存好原先的 window.onload,把它放入新的 window.onload 里执行:

代码示例

window.onload = function () {
        console.log(1);
    }
    var _onload = window.onload || function () { };
    window.onload = function () {
        _onload();
        console.log(2);
    }
    输出:1 2

这样的代码当然是符合开放-封闭原则的,我们在增加新功能的时候,确实没有修改原来的window.onload 代码,但是这种方式存在以下两个问题。

  • 必须维护_onload 这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。
  • 其实还遇到了 this 被劫持的问题,在 window.onload 的例子中没有这个烦恼,是因为调用普通函数_onload 时,this 也指向 window,跟调用 window.onload 时一样(函数作为对象的方法被调用时,this 指向该对象,所以此处 this 也只指向 window)。现在把 window.onload换成 document.getElementById,比如:
var _getElementById = document.getElementById;
    document.getElementById = function (id) {
        console.log(1);
        return _getElementById(id); // (1) 
    }
    var button = document.getElementById('button');
    // 错误: Uncaught TypeError: Illegal invocation

使用AOP 装饰函数
所谓AOP(面向切面编程)就是:把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个松耦合和高复用性的系统。如下:

window.onload = function () {
        console.log(1);
    }
   //AOP实现
   /**通过 Function.prototype.apply 来动态传入正确的 this,保证了函数在被装饰
之后,this 不会被劫持。*/
 Function.prototype.after = function (afterfn) {
        var __self = this;
        return function () {
            var ret = __self.apply(this, arguments);
            afterfn.apply(this, arguments);
            return ret;
        }
    };
    window.onload = (window.onload || function () { }).after(function () {
        console.log(2);
    }).after(function () {
        console.log(3);
    }).after(function () {
        console.log(4);
    });
    //输出 1 2 3 4

note

装饰者模式在框架开发中十分有用。作为框架作者,我们希望框架里的函数提供的是一些稳定而方便移植的功能,那些个性化的功能可以在框架之外动态装饰上去,这可以避免为了让框架拥有更多的功能,而去使用一些 if、else 语句预测用户的实际需要。

状态模式

概念

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。(通俗点说就是类的行为是基于它的状态改变的)

场景

酒店里有一种电灯,这种电灯只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。现在通过代码来完成这种新型电灯的制造:

代码对比示例

//一般情况我们可以这样写
var Light = function () {
        this.state = 'off'; // 给电灯设置初始状态 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.buttonWasPressed();
        }
    };
    Light.prototype.buttonWasPressed = function () {
        if (this.state === 'off') {
            console.log('弱光');
            this.state = 'weakLight';
        } else if (this.state === 'weakLight') {
            console.log('强光');
            this.state = 'strongLight';
        } else if (this.state === 'strongLight') {
            console.log('关灯');
            this.state = 'off';
        }
    };
    var light = new Light();
    light.init();
    // 按下按钮 分别输出 弱光 强光 关灯

通过状态模式实现

 // OffLightState:
    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();
     // 按下按钮 分别输出 弱光 强光 关灯

通过状态模式改造后的代码,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码,无需过多的if else条件语句。当我们需要为 light 对象增加一种新的状态时,只需要增加一个新的状态类,然后在 Light 构造函数里新增一个 superStrongLightState 对象:

 // 新增超强光
    var SuperStrongLightState = function (light) {
        this.light = light;
    };
    SuperStrongLightState.prototype.buttonWasPressed = function () {
        console.log('关灯');
        this.light.setState(this.light.offLightState);
  };
 //并在Light 构造函数里新增一个 superStrongLightState 对象:
  this.superStrongLightState = new SuperStrongLightState(this); // 新增 superStrongLightState 对象
 //最后改变状态类之间的切换规则 强光->超强光->关闭
 // StrongLightState:
    var StrongLightState = function (light) {
        this.light = light;
    };
    StrongLightState.prototype.buttonWasPressed = function () {
        console.log('超强光'); // strongLightState 对应的行为
        this.light.setState(this.light.superStrongLightState); // 切换状态SuperStrongLightState
    };

JavaScript 版本的状态机

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

JavaScript大法好

适配器模式

概念

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。

场景

当我们向 googleMap 和 baiduMap 都发出“显示”请求时,googleMap和 baiduMap 分别以各自的方式在页面中展现了地图:

代码示例

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

这段程序得以顺利运行的关键是 googleMap 和 baiduMap 提供了一致的 show 方法,如果不同呢?比如叫display。此时我们可以通过增加 baiduMapAdapter 来解决问题:

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

note

适配器模式是一对相对简单的模式。在本书提到的设计模式中,有一些模式跟适配器模式的结构非常相似,比如装饰者模式、代理模式和外观模式。这几种模式都属于“包装模式”,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。

结尾

软件设计模式是一种前人的总结和遇到问题后的解决方案,不必生搬硬套,特别是在JavaScript中。在了解设计模式前个人更加推荐先看看设计原则和编程技巧篇。
注:本文所有内容都是来自《javascript设计模式与开发实战》书中的总结和部分衍生,便于自我学习,所有代码块都可运行。欢迎讨论