JavaScirpt 设计模式(四)— 行为型设计模式

60 阅读8分钟

用于不同对象之间职责划分或者算法抽象。
不仅涉及类和对象,还涉及了类或者对象之间的交流模式并加以实现。

模板方法模式

父类中定义一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
父类即是将多个模型对象抽象归一,提取出的一个最基本模板。
模板方法中的步骤越多, 其维护工作就可能会越困难。

// 格式化字符串
function formateString(str, data) {
    function formateString(str, data) {
        // 该函数的第一个参数是匹配模式的字符串。接下来的参数是与模式中的子表达式匹配的字符串,可以有 0 个或多个这样的参数
        return str.replace(/\{#(\w+)#\}/g, function (match, key) {
            return typeof data[key] === 'undefined' ? '' : data[key];
        });
    }
}
// 创建导航栏模板
let Nav = function (data) {
    this.item = '<a href="{#href#}" title="{#title#}">{#name#}</a>';
    this.html = '';
    for (let i = 0, ; i < data.length; i++) {
        this.html += formateString(this.item, data[i]);
    }
    return this.html;
};
// 带有链接地址的导航
let LinkNav = function (data) {
    let tpl = '<span>{#link#}</span>';
    for (let i = 0; i < data.length; i++) {
        data[i].name += formateString(tpl, data[i]);
    }
    return Nav.call(this, data);
};
let nav = document.createElement('div');
document.body.appendChild(nav);
nav.innerHTML = LinkNav([
    {
        href: 'www.baidu.com',
        name: '百度',
        title: '百度一下你就知道',
        link: '666',
    },
    {
        href: 'www.google.com',
        name: '谷歌',
        title: '谷歌一下你就知道',
        link: '888',
    },
]);

观察者模式

又称为发布-订阅模式或消息机制,定义了一种依赖关系,解决了主体对象和观察者之间功能的耦合。
拥有一些值得关注的状态的对象通常被称为目标,由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者(publisher),所有希望关注发布者状态变化的其他对象被称为订阅者(subscribers)。
优点是遵循开闭原则,你无需修改发布者代码就能引入新的订阅者,可以在运行时建立对象之间的联系。
缺点是订阅者的通知顺序是随机的。

// 观察者放在闭包中·,页面加载时立即执行
let observer = (function () {
    // 消息对列作为静态私有变量,防止泄露而被篡改
    let _messages = {};
    return {
        // 注册接口:
        on: function () {},
        // 发布接口
        emit: function () {},
        // 移除接口
        off: function () {},
    };
})();
function on(type, fn) {
    if (typeof _messages[type] === 'undefined') {
        _messages[type] = [fn];
    } else {
        _messages[type].push(fn);
    }
}
function emit(type, args) {
    if (!_messages[type]) {
        return;
    }
    let events = {
            type: type,
            args: args || {},
        },
        i = 0,
        len = _messages.length;
    for (; i < len; i++) {
        _messages[type][i].call(this, events);
    }
}
function off(type, fn) {
    if (_messages[type] instanceof Array) {
        let i = _messages[type].len - 1;
        for (; i > 0; i--) {
            _messages[type][i] === fn && _messages[type].splice(i, i);
        }
    }
}

状态模式

一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
其主要目的在于将条件判断的不同结果转化为状态对象的内部状态,简化分支判断流程。
状态模式与有限状态机的概念紧密相关。

// 根据投票结果选出本月最美图片
function showResult() {
    if (result === 0) {
        // 处理结果0
    } else if (result === 1) {
        // 处理结果1
    } else {
        // ...
    }
}
let ResultState = (function () {
    let states = {
        state0: function () {
            // 处理结果0;
        },
        state1: function () {
            // 处理结果1;
        },
        // ...
    };
    function show(result) {
        states['state' + result] && states['state' + result]();
    }
    return {
        show: show,
    };
})();
Result.show(1);

策略模式

将定义一系列算法封装起来,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换,具备一定独立性,不会随客户端变化而变化。
与状态模式相比,策略模式不需要管理状态,状态间距没有依赖关系,策略对象内部保存的是相互独立的一些算法。

// 年货节商品打折促销活动
let PriceStrategy = function () {
    // 内部算法对象
    let strategy = {
        // 满100返30
        return30: function (price) {
            // + price 转化为数字类型
            return +price + paseInt(price / 100) * 30;
        },
        // 满100返50
        return50: function (price) {
            return +price + paseInt(price / 100) * 50;
        },
        // ...
    };
    return function (algorithm, price) {
        return strategy[algorithm] && strategy[algorithm](price);
    };
};
let price = PriceStrategy('return30', '314.5');

责任链模式

允许将请求沿着处理者链进行发送,收到请求后,每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。
定义了请求的方向,通过多个对象对请求的传递,实现一个复杂逻辑操作,将复杂需求模块化、细粒化。

// 实现一个信息表单提交需求,包括输入提示、提交验证
// 请求模块
let sendData = function () {
    // ...
};
// 响应数据处理模块
let dealData = function () {
    // ...
};
// 创建组件模块
let createMsg = function () {
    // ...
};

命令模式

将请求与实现解耦并封装成独立对象,从而使不同的请求对客户端实现参数化。
根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,能实现可撤销操作。

// 在模块内命令式创建图片视图
let viewCommand = (function () {
    let tpl = {
            product: ['<div>', '<img src="{#src#}"/>', '<p>{#title#}</p>', '</div>'].join(''),
            title: [
                '<div>',
                '<div>',
                '<h2>{#title#}</h2>',
                '<p>{#tips#}</p>',
                '</div>',
                '</div>',
            ].join(''),
        },
        html = '';
    function formateString(str, obj) {
        return str.replace(/\{#(\w+)#\}/g, function (match, key) {
            return obj[key];
        });
    }
    let action = {
        create: function (data, view) {
            // 如果是数组
            if (data.length) {
                for (const i in data) {
                    html += formateString(tpl[view], data[i]);
                }
            } else {
                html += formateString(tpl[view], data);
            }
        },
        display: function (container, data, view) {
            if (data) {
                this.create(data, view);
            }
            document.getElementById(container).innerHTML = html;
            html = '';
        },
    };
    return function exec(msg) {
        msg.params =
            Object.prototype.toString.call(msg.params) === '[object Array]'
                ? msg.params
                : [msg.params];
        action[msg.command].apply(action, msg.params);
    };
})();

const productData = [
    {
        src: 'https://images.unsplash.com/photo-1659651224631-1564cdf45396?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwyNHx8fGVufDB8fHx8&auto=format&fit=crop&w=500&q=60',
        title: '柠檬',
    },
    {
        src: 'https://images.unsplash.com/photo-1659608877046-340042309b3d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw2MXx8fGVufDB8fHx8&auto=format&fit=crop&w=500&q=60',
        title: '蔓越莓',
    },
    {
        src: 'https://images.unsplash.com/photo-1659598468697-b029ca1d9bf0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHw5NHx8fGVufDB8fHx8&auto=format&fit=crop&w=500&q=60',
        title: '鲜花',
    },
];

const titleData = [{ title: '缤纷灿烂的夏季', tips: '是香甜与清爽的相碰' }];

viewCommand({
    command: 'display',
    params: ['title', titleData, 'title'],
});

viewCommand({
    command: 'display',
    params: ['product', productData, 'product'],
});

访问者模式

针对对象结构中的元素,定义在不改变对象的前提下访问结构中元素的新方法,将算法与其所作用的对象隔离开来。
解决数据与数据操作方法之间的耦合,适用于数据稳定、操作方法易变的场景。

// 类数组访问器,像操作数组一样操作对象
let Visitor = (function () {
    return {
        splice: function () {
            // 从原参数第二个参数开始算起
            const args = Array.prototype.splice.call(arguments, 1);
            return Array.prototype.splice.apply(arguments[0], args);
        },
        push: function () {
            // 强化类数组,使它拥有length属性
            const len = arguments[0].length || 0;
            const args = this.splice(arguments, 1);
            arguments[0].length = len + arguments.length - 1;
            return Array.prototype.push.apply(arguments[0], args);
        },
        pop: function () {
            return Array.prototype.pop.apply(arguments[0]);
        },
    };
})();
let a = new Object();
console.log('len', a.length);
Visitor.push(a, 'test1', 'test2');
console.log('len', a.length);
console.log('a', a);

中介者模式

通过中介对象封装一系列对象之间的交互,使对象之间不再相互引用,降低它们之间的耦合。
中介者模式建议停止组件之间的直接交流并使其相互独立,仅依赖于一个中介者类, 无需与多个其他组件相耦合,通过中介者对象重定向调用行为, 以间接的方式进行合作。

// 中介者或者说中央事件总线
const Mediator = (function () {
    let _msg = {};
    return {
        register: function (type, action) {
            if (_msg[type]) {
                _msg[type].push(action);
            } else {
                // 建立消息容器
                _msg[type] = [];
                _msg[type].push(action);
            }
        },
        send: function (type) {
            if (_msg[type]) {
                for (const i of _msg[type]) {
                    i && i();
                }
            }
        },
    };
})();
// 单元测试
Mediator.register('demo', function () {
    console.log('first');
});
Mediator.register('demo', function () {
    console.log('second');
});
Mediator.send('demo');

备忘录模式

在不破坏封装对象的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者恢复到以前某个状态。
在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

// 创建一个分页数据备忘录类
let Page = (function () {
    let cache = {};
    const getData = ({ page }) => {
        return { page, data: new Array(10).fill(page * 10) };
    };
    const showPage = (page, data) => {
        console.log(`this ${page} page is ${data}`);
    };
    return function (page, fn) {
        if (cache[page]) {
            showPage(page, cache[page]);
            fn && fn();
        } else {
            // 异步数据改同步获取
            const res = getData({ page });
            if (res.data) {
                showPage(page, res.data);
                cache[page] = res.data;
            }
        }
    };
})();
Page(1);
const back = function () {
    console.log('is back');
};
Page(1, back);

迭代器模式

在不暴露对象内部结构的同时,可以顺序地访问聚合对象内部的元素。
迭代器是优化循环语句的一种可行方案,使得程序清晰易读。

// 同步变量
const A = {
    testData: {
        data0: {
            test: 'data0',
        },
        data1: 'test data1',
    },
};
// 同步变量迭代器,某些属性存在未知时,通过迭代法减少检验
const AGetter = function (key) {
    if (!A) return undefined;
    let result = { ...A };
    key = key.split('.');
    for (const i in key) {
        if (result[key[i]] !== undefined) {
            result = result[key[i]];
        } else {
            return undefined;
        }
    }
    return result;
};
console.log(AGetter('testData.data0.test')); // data0
console.log(AGetter('testData.data1.test')); // undefined

// 分支循环嵌套问题
function dealData(type, data) {
    let list = data;
    for (const i in list) {
        switch (type) {
            case 'typeOne':
                list[i].data = 'red';
                break;
            case 'typeTwo':
                list[i].data = 'yellow';
                break;
            case 'typeThree':
                list[i].data = 'green';
            default:
                list[i].data = 'gray';
        }
    }
}
// 结合策略模式和迭代器模式,减少无用的分支判断
// 看起来代码好像多了,实际逻辑进行了上数据与操作方法解耦
function dealData1(type, data) {
    let list = data;
    const deal = (function () {
        let methods = {
            typeOne: function (i) {
                list[i].data = 'red';
            },
            typeTwo: function (i) {
                list[i].data = 'yellow';
            },
            typeThree: function (i) {
                list[i].data = 'green';
            },
            default: function (i) {
                list[i].data = 'gray';
            },
        };
        return function (type) {
            return methods[type] || methods['default'];
        };
    })();
    function eachData(fn) {
        for (const i in list) {
            fn(i);
        }
    }
    eachData(deal(type));
    return list;
}

解释器模式

对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。
重点在于将用户需求、描述性语句、几次功能的提取对象,形成一套语言规则,即是解释器模式需要处理的事情。

// 需求:获取元素在页面中所处路径
let interpreter = (function () {
    // 获取兄弟元素名称统计,DOM1级定义了一个Node接口,该接口将由DOM中的所有节点类型实现
    function getSubbingName(node) {
        if (node.previousSibling) {
            let name = '', // 返回兄弟元素名称
                count = 1, // 相邻兄弟元素相同名称元素个数
                nodeName = node.nodeName,
                // 前一个兄弟节点
                sibling = node.previousSibling;
            while (sibling) {
                // 如果节点为元素节点, 并且与前一个兄弟元素类型相同
                if (
                    sibling.nodeType == 1 &&
                    sibling.nodeType === node.nodeType &&
                    sibling.nodeName
                ) {
                    if (sibling.nodeName == nodeName) {
                        // 去除数字字符
                        name = name.replace(/\d+/, '');
                        name += ++count;
                    } else {
                        // 重置相同相邻节点名称与个数
                        count = 1;
                        name += '|' + sibling.nodeName.toUpperCase();
                    }
                }
                sibling = sibling.previousSibling;
            }
            return name;
        } else {
            return '';
        }
    }
    return function (node, wrapper) {
        let path = [],
            wrap = wrapper || document;
        if (node == wrap) {
            if (wrap.nodeType == 1) {
                path.push(wrap.nodeName.toUpperCase());
            }
            return path;
        }
        if (node.parentNode !== wrap) {
            // 函数被调用时,它的arguments.callee对象就会指向自身
            path = arguments.callee(node.parentNode, wrap);
        } else {
            if (wrap.nodeType == 1) {
                path.push(wrap.nodeName.toUpperCase());
            }
        }
        const subbingName = getSubbingName(node);
        if (node.nodeType == 1) {
            path.push(node.nodeName.toUpperCase() + subbingName);
        }
        return path;
    };
})();
let path = interpreter(document.getElementById('test'));
console.log(path + '\n' + path.join('>'));