JavaScript设计模式(四):行为型设计模式-模板方法模式、观察者模式(发布订阅模式)、状态模式、策略模式、职责链模式、命令模式、访问者模式、中介者模式、备忘录模式、迭代器模式、解释器模式

照猫画虎-模板方法模式

模板方法模式(Template Method)

父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时可重新定义算法中某些实现步骤。

需求:定义一个全局能用的弹出框

在这里插入图片描述

<style>
    * {
      
      
        padding: 0;
        margin: 0;
        outline: none;
    }

    /* 提示框容器 */
    .alert-box {
      
      
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 420px;
        background-color: #fff;
        border-radius: 4px;
        border: 1px solid #ebeef5;
        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
        display: none;
    }

    /* Header部分 */
    .alert-box .alert-box__header {
      
      
        position: relative;
        padding: 15px 15px 10px;
    }

    .alert-box .alert-box__header .alert-box__title {
      
      
        padding-left: 0;
        margin-bottom: 0;
        font-size: 18px;
        line-height: 1;
        color: #303133;
    }

    .alert-box .alert-box__header .alert-box__headerbtn {
      
      
        position: absolute;
        top: 15px;
        right: 15px;
        padding: 0;
        border: none;
        outline: none;
        background: transparent;
        font-size: 16px;
        cursor: pointer;
    }

    /* content部分 */
    .alert-box .alert-box__content {
      
      
        padding: 10px 15px;
        color: #606266;
        font-size: 14px;
    }

    /* 操作部分 */
    .alert-box .alert-box__footer {
      
      
        padding: 5px 15px 10px 15px;
        text-align: right;
    }

    /* 按钮部分 */
    .btn {
      
      
        cursor: pointer;
        text-align: center;
        box-sizing: border-box;
        outline: none;
        padding: 0 10px;
        height: 32px;
        line-height: 16px;
        font-size: 14px;
        border-radius: 4px;
    }

    .btn+.btn {
      
      
        margin-left: 5px;
    }

    .btn.primary__btn {
      
      
        border: 1px solid #409eff;
        background-color: #409eff;
        color: #fff;
    }

    .btn.default__btn {
      
      
        border: 1px solid #b6b8bb;
        background-color: #ffffff;
        color: #333;
    }

    .btn.warning__btn {
      
      
        border: 1px solid #f56c6c;
        background-color: #f56c6c;
        color: #fff;
    }

    .title-center{
      
      
        text-align: center;
    }
</style>

<body>
    <fieldset style="margin: 20px;padding: 20px;">
        <legend>基础提示框</legend>
        <button id="show-btn" class="btn primary__btn">显示</button>
        <button id="hide-btn" class="btn primary__btn">隐藏</button>
    </fieldset>
    <fieldset style="margin: 20px;padding: 20px;">
        <legend>根据模板创建自定义提示框</legend>
        <button id="show-custom-btn" class="btn primary__btn">显示</button>
        <button id="hide-custom-btn" class="btn primary__btn">隐藏</button>
    </fieldset>
</body>
/**
 * 基础提示框模板类
 * <div class="alert-box">
 *     <div class="alert-box__header">
 *         <div class="alert-box__title"><b>提示</b></div>
 *         <button class="alert-box__headerbtn" type="button"><i>X</i></button>
 *     </div>
 *     <div class="alert-box__content">基本提示框</div>
 *     <div class="alert-box__footer">
 *         <button class="btn default__btn">取消</button>
 *         <button class="btn primary__btn">确认</button>
 *         <button class="btn warning__btn">删除</button>
 *     </div>
 * </div>
 */
const Alert = function (data) {
    
    
    // 没有数据则返回,防止后面程序执行
    if (!data) return;

    /**
     * 创建提示框面板
     */
    this.panelNode = document.createElement('div');
    this.panelNode.className = 'alert-box';

    /**
     * 创建header组件容器
     */
    this.headerNode = document.createElement('div');
    this.headerNode.className = 'alert-box__header';

    /**
     * 创建header内标题组件
     */
    this.title = data.title;
    this.titleNode = document.createElement('div');
    this.titleNode.className = 'alert-box__title';
    this.titleNode.innerHTML = data.title || '<b>提示</b>';

    /**
     * 创建header内关闭按钮组件
     */
    this.showCloseBtn = data.showCloseBtn === undefined ? true : data.showCloseBtn;
    this.closeIcon = data.closeIcon;
    this.closeBtnNode = document.createElement('button');
    this.closeBtnNode.className = 'alert-box__headerbtn';
    this.closeBtnNode.innerHTML = data.closeIcon || '<i>X</i>';

    /**
     * 创建内容组件
     */
    this.content = data.content;
    this.contentNode = document.createElement('div');
    this.contentNode.className = 'alert-box__content';
    this.contentNode.innerHTML = data.content || '';

    /**
     * 创建footer组件
     */
    this.footerNode = document.createElement('div');
    this.footerNode.className = 'alert-box__footer';

    /**
     * 创建确认按钮
     */
    this.showConfirmBtn = data.showConfirmBtn === undefined ? true : data.showConfirmBtn;
    this.confirmText = data.confirmText;
    this.confirmBtnNode = document.createElement('button');
    this.confirmBtnNode.className = 'btn primary__btn';
    this.confirmBtnNode.innerText = data.confirmText || '确认';

    /**
     * 创建取消按钮
     */
    this.showCancelBtn = data.showCancelBtn === undefined ? true : data.showCancelBtn;
    this.cancelText = data.cancelText;
    this.cancelBtnNode = document.createElement('button');
    this.cancelBtnNode.className = 'btn default__btn';
    this.cancelBtnNode.innerText = data.cancelText || '取消';

    /**
     * 监听按钮回调
     */
    this.handleClose = data.handleClose || function () {
    
     };
    this.handleConfirm = data.handleConfirm || function () {
    
     };
    this.handleCancel = data.handleCancel || function () {
    
     };
    this.handleDelete = data.handleDelete || function () {
    
     };
};

// 基础提示框 的 原型方法
Alert.prototype = {
    
    
    // 初始化基础提示框
    init() {
    
    
        // header内插入组件
        this.headerNode.appendChild(this.titleNode);
        this.showCloseBtn && this.headerNode.appendChild(this.closeBtnNode);
        // 提示框内插入header组件
        this.panelNode.appendChild(this.headerNode);
        // 提示框内插入content组件
        this.panelNode.appendChild(this.contentNode);
        // 提示框底部添加 确认、取消 按钮组件
        this.showConfirmBtn && this.footerNode.appendChild(this.confirmBtnNode);
        this.showCancelBtn && this.footerNode.appendChild(this.cancelBtnNode);
        // 提示框内插入footer组件
        this.panelNode.appendChild(this.footerNode);
        // 插入页面中
        document.body.appendChild(this.panelNode);

        // 绑定事件
        this.bindEvent();

        return this;
    },
    bindEvent() {
    
    
        const that = this;
        // 关闭按钮点击事件
        this.closeBtnNode.onclick = function () {
    
    
            // 执行关闭方法
            that.handleClose();
            // 隐藏弹层
            that.hide();
        };
        // 确定按钮点击事件
        this.confirmBtnNode.onclick = function () {
    
    
            // 执行确认方法
            that.handleConfirm();
            // 隐藏弹层
            that.hide();
        };
        // 取消按钮点击事件
        this.cancelBtnNode.onclick = function () {
    
    
            // 执行取消方法
            that.handleCancel();
            // 隐藏弹层
            that.hide();
        };
    },
    // 隐藏弹层方法
    hide() {
    
    
        this.panelNode.style.display = 'none';
    },
    // 显示弹层方法
    show() {
    
    
        this.panelNode.style.display = 'block';
    }
};
// 实例化出一个基础弹出框
let baseAlert = new Alert({
    
    
    title: '<b>提示(ง •_•)ง:</b>',
    showCloseBtn: true,
    content: '请点击您要对该条数据执行的操作按钮!',
    showConfirmBtn: true,
    showCancelBtn: true,
    handleClose() {
    
    
        console.log('关闭');
    },
    handleConfirm() {
    
    
        console.log('确认');
    },
    handleCancel() {
    
    
        console.log('取消');
    },
}).init();

// 显示
document.getElementById('show-btn').onclick = function () {
    
    
    baseAlert.show();
};

// 隐藏
document.getElementById('hide-btn').onclick = function () {
    
    
    baseAlert.hide();
};
/**
 * 继承自基础提示框的自定义提示框
 */
const CustomALert = function (data) {
    
    
    // 构造函数设计模式
    Alert.call(this, data);
    // 修改模板内的组件
    this.titleNode.className += ' title-center';
};
// 继承基本提示框方法
CustomALert.prototype = new Alert();
CustomALert.prototype.init = function () {
    
    
    // 插入标题
    this.titleNode.innerHTML += ' ヾ(≧▽≦*)o'
    this.panelNode.insertBefore(this.titleNode, this.panelNode.firstChild);
    // 调用父类原型上的方法
    Alert.prototype.init.call(this);
    return this;
};
let customAlert = new CustomALert({
    
    
    title: '<b>提示(ง •_•)ง</b>',
    showCloseBtn: true,
    content: '请点击您要对该条数据执行的操作按钮!',
    showConfirmBtn: true,
    showCancelBtn: true,
    handleClose() {
    
    
        console.log('关闭');
    },
    handleConfirm() {
    
    
        console.log('确认');
    },
    handleCancel() {
    
    
        console.log('取消');
    },
}).init();

// 显示
document.getElementById('show-custom-btn').onclick = function () {
    
    
    customAlert.show();
};

// 隐藏
document.getElementById('hide-custom-btn').onclick = function () {
    
    
    customAlert.hide();
};

特点:

模板方法核心在于对方法的重用,它将核心方法封装在基类中,让子类继承基类的方法实现基类方法的共享,达到方法共用

当然这种设计模式也将导致基类控制子类必须遵守某些法则。这是一种行为的约束。当然为了让行为的约束更可靠基类中封装的方法通常是不变的算法,或者具有稳定的调用方式

子类继承的方法亦是可以扩展的,这就要求对基类继承的方法进行重写。当然为了更好地实践,我们通常要控制这种拓展,这样才能让基类对子类有更稳健的束缚力。然而子类对自身私有行为的拓展还是很有必要的。

通信卫星-观察者模式

观察者模式(Observer)

又被称作发布-订阅者模式消息机制,定义了一种依赖关系,解决了主体对象与观察者之间功能的耦合,以及模块间通信问题

实现评论模块

  • 需求:
    1. 当用户发布评论时,会在评论展示模块末尾处追加新的评论;
    2. 与此同时用户的消息模块的消息数量也会递增;
    3. 如果用户删除留言区的信息时,用户的消息模块消息数量也会递减;
  • 存在的问题:
    • 这些模块的代码是三位不同的工程师写的,都写在自己的独立闭包模块里,现在我要完成我的需求,又不想将他们的模块合并到一起
<style>
    ul {
      
      
        list-style: none;
        border: 1px solid #333;
        padding: 5px;
        min-height: 100px;
    }

    ul li {
      
      
        word-break: break-all;
        padding: 5px;
        text-align: justify;
    }

    ul li::before,
    ul li::after {
      
      
        content: "";
        display: block;
        clear: both;
    }

    ul li button {
      
      
        float: right;
    }

    ul li:nth-child(2n) {
      
      
        background-color: #ccc;
    }

    ul li:nth-child(2n + 1) {
      
      
        background-color: #aaa;
    }
</style>

<body>
    <input id="user_input" />
    <button id="user_submit">Submit</button>
    <span id="msg_num"></span>
    <ul id="msg_box"></ul>
    <script src="./index.js"></script>
</body>

实现三位工程师的功能

在这里插入图片描述

// 原三位工程师实现的功能逻辑
function $(id) {
    
    
    return document.getElementById(id);
}

/**
 * 工程师 A
 *      实现了消息列表追加以及删除消息功能
 */
; (function () {
    
    
    /**
     * 追加一则消息,并可对消息进行删除
     * @param {string} msg 消息内容
     */
    function addMsgItem({
     
      msg }) {
    
    
        // 获取评论容器,创建消息条目,创建删除按钮
        const ul = $('msg_box'), li = document.createElement('li'), btn = document.createElement('button');
        // 配置删除按钮文字
        btn.innerText = 'Delete';
        // 删除点击事件 测试执行 ---> 点击进行测试
        btn.onclick = function () {
    
    
            ul.removeChild(li);
        };
        // 添加消息内容
        li.innerText = msg;
        // 添加删除按钮
        li.appendChild(btn);
        // 追加一则消息
        ul.appendChild(li);
    }
    // 测试执行
    addMsgItem({
    
    msg: 'Hello!'});
    addMsgItem({
    
    msg: 'Hi!'});
    addMsgItem({
    
    msg: '你好呀!'});
    addMsgItem({
    
    msg: '我不好呀!'});
    addMsgItem({
    
    msg: '你爱好不好呀!'});
}());

/**
 * 工程师 B
 *      实现了消息数目累加减功能
 */
; (function () {
    
    
    /**
     * 修改消息数量
     * @param {number} count 累加减数量 -1和1
     */
    function changeMsgCount({
     
      count }) {
    
    
        $('msg_num').innerText = Number($('msg_num').innerText) + count;
    }
    // 测试执行
    changeMsgCount({
    
    count: 1});  // 1
    changeMsgCount({
    
    count: 1});  // 2
    changeMsgCount({
    
    count: -1}); // 1
    changeMsgCount({
    
    count: 1});  // 2
}());

/**
 * 工程师 C
 *      实现了消息提交的功能
 */
; (function () {
    
    
    /**
     * 用户点击提交按钮
     *      测试执行 ---> 点击进行测试
     */
    $('user_submit').onclick = function () {
    
    
        // 获取用户输入框中输入的信息
        const msg = $('user_input');
        // 如果输入框内容为空则退出函数
        if (msg.value === '') return;
        console.log(msg.value);
        // 点击完成后将输入框置为空
        msg.value = '';
    };
    // 测试执行 ---> 点击进行测试
}());

使用观察者模式整合这三个功能

在这里插入图片描述

function $(id) {
    
    
    return document.getElementById(id);
}

/**
 * 定义观察者
 */
const Observer = (function () {
    
    
    // 防止消息队列暴漏而被篡改故将消息容器作为静态私有变量保存
    let __messages = {
    
    };
    return {
    
    
        __messages,
        /**
         * 注册消息接口 - 暂存
         * @param {string} type 消息类型
         * @param {Function} fn 消息回调
         */
        regist(type, fn) {
    
    
            // 如果此消息不存在则应该创建一个该消息类型
            if (typeof __messages[type] === 'undefined') {
    
    
                // 将动作推入到该消息对应的动作执行队列中
                __messages[type] = [fn];
            }
            // 如果此消息存在
            else {
    
    
                // 将动作方法推入该消息对应的动作执行序列中
                __messages[type].push(fn);
            }
            return this;
        },
        /**
         * 发布消息接口 - 调用
         * @param {string} type 消息类型
         * @param {object} args 消息携带数据
         */
        fire(type, args) {
    
    
            // 如果该消息没有被注册,则返回
            if (!__messages[type]) return;
            // 遍历消息动作
            for (let i = 0; i < __messages[type].length; i++) {
    
    
                // 依次执行注册的消息对应的动作序列
                __messages[type][i].call(this, args);
            }
        },
        /**
         * 移除消息接口 - 移除
         * @param {string} type 消息类型
         * @param {Function} fn 消息回调
         */
        remove(type, fn) {
    
    
            // 如果消息动作队列存在
            if (__messages[type] instanceof Array) {
    
    
                // 从最后一个消息动作遍历
                for (let i = __messages[type].length - 1; i >= 0; i--) {
    
    
                    // 如果存在该动作则在消息动作序列中移除相应动作
                    __messages[type][i] === fn && __messages[type].splice(i, 1);
                }
            }
        }
    };
}());

/**
 * 工程师 A
 *      实现了消息列表追加以及删除消息功能
 */
; (function () {
    
    
    /**
     * 追加一则消息,并可对消息进行删除
     * @param {string} msg 消息内容
     */
    function addMsgItem({
     
      msg }) {
    
    
        // 获取评论容器,创建消息条目,创建删除按钮
        const ul = $('msg_box'), 
            li = document.createElement('li'), 
            btn = document.createElement('button');
        // 配置删除按钮文字
        btn.innerText = 'Delete';
        // 删除点击事件
        btn.onclick = function () {
    
    
            ul.removeChild(li);
            Observer.fire('减少操作', {
    
     count: -1 });
        };
        // 添加消息内容
        li.innerText = msg;
        // 添加删除按钮
        li.appendChild(btn);
        // 追加一则消息
        ul.appendChild(li);
    }

    // 注册这个功能
    Observer.regist('新增操作', addMsgItem);
}());

/**
 * 工程师 B
 *      实现了消息数目累加减功能
 */
; (function () {
    
    
    /**
     * 修改消息数量
     * @param {number} count 累加减数量 -1和1
     */
    function changeMsgCount({
     
      count }) {
    
    
        console.log(count);
        $('msg_num').innerText = Number($('msg_num').innerText) + count;
    }

    // 分类别注册这个功能(新增的操作作为一个分类,减少的操作作为一个类别)
    Observer.regist('新增操作', changeMsgCount)
        .regist('减少操作', changeMsgCount);
}());

/**
 * 工程师 C
 *      实现了消息提交的功能
 */
; (function () {
    
    
    /**
     * 用户点击提交按钮
     */
    $('user_submit').onclick = function () {
    
    
        // 获取用户输入框中输入的信息
        const msg = $('user_input');
        // 如果输入框内容为空则退出函数
        if (msg.value === '') return;
        Observer.fire('新增操作', {
    
     msg: msg.value, count: 1 });
        // 点击完成后将输入框置为空
        msg.value = '';
    };
}());

console.log(Observer);

理解:

将函数进行分类并缓存成队列(订阅),然后当需要执行指定操作时依次调用同分类的函数方法(发布)。

  • 此模块的实现:
    • regist
      • 订阅 新增类 函数方法(包括发送消息递增消息数目);
        • 发送消息: Observer.regist('新增操作', addMsgItem);
        • 递增消息数目: Observer.regist('新增操作', changeMsgCount);
      • 订阅 减法类 函数方法(包括递减消息数目);
        • 递减消息数目: Observer.regist('减少操作', changeMsgCount);
    • fire
      • 发布 订阅的 新增类 函数方法
        • Observer.fire('新增操作', { msg: msg.value, count: 1 });
      • 发布 订阅的 减法类 函数方法
        • Observer.fire('减少操作', { count: -1 });

对象间解耦

  • 需求(考试):
    1. 学生考试获得最终的考试结果;
    2. 学生也可以放弃某一学科的考试;
    3. 教师发布考试最终的结果表;

在这里插入图片描述

/**
 * 定义观察者
 */
const Observer = (function () {
    
    
    // 防止消息队列暴漏而被篡改故将消息容器作为静态私有变量保存
    let __messages = {
    
    };
    return {
    
    
        __messages,
        /**
         * 注册消息接口 - 暂存
         * @param {string} type 消息类型
         * @param {Function} fn 消息回调
         */
        regist(type, fn) {
    
    
            // 如果此消息不存在则应该创建一个该消息类型
            if (typeof __messages[type] === 'undefined') {
    
    
                // 将动作推入到该消息对应的动作执行队列中
                __messages[type] = [fn];
            }
            // 如果此消息存在
            else {
    
    
                // 将动作方法推入该消息对应的动作执行序列中
                __messages[type].push(fn);
            }
            return this;
        },
        /**
         * 发布消息接口 - 调用
         * @param {string} type 消息类型
         * @param {object} args 消息携带数据
         */
        fire(type, args) {
    
    
            // 如果该消息没有被注册,则返回
            if (!__messages[type]) return;
            // 遍历消息动作
            for (let i = 0; i < __messages[type].length; i++) {
    
    
                // 依次执行注册的消息对应的动作序列
                __messages[type][i].call(this, args);
            }
        },
        /**
         * 移除消息接口 - 移除
         * @param {string} type 消息类型
         * @param {Function} fn 消息回调
         */
        remove(type, fn) {
    
    
            // 如果消息动作队列存在
            if (__messages[type] instanceof Array) {
    
    
                // 从最后一个消息动作遍历
                for (let i = __messages[type].length - 1; i >= 0; i--) {
    
    
                    // 如果存在该动作则在消息动作序列中移除相应动作
                    __messages[type][i] === fn && __messages[type].splice(i, 1);
                }
            }
        }
    };
}());
/**
 * 学生类 订阅者
 * @param {string} name 姓名
 */
const Student = function (name) {
    
    
    this.name = name;
    this.getResult = () => console.log(`${
      
      this.name} 的结果为:${
      
      this.result} 分!`);
};
Student.prototype = {
    
    
    /**
     * 回答的试卷
     * @param {string} test 试卷
     * @param {number} result 结果
     */
    setResult(test, result) {
    
    
        this.result = result;
        Observer.regist(test, this.getResult);
    },
    /**
     * 放弃方法
     * @param {string} test 试卷
     */
     giveUp(test) {
    
    
        console.log(`${
      
      this.name} 放弃了 ${
      
      test} ~~~`);
        Observer.remove(test, this.getResult);
    }
};

/**
 * 教师类 发布者
 */
const Teacher = function () {
    
     };
Teacher.prototype = {
    
    
    // 发布试卷
    distributeTest(test) {
    
    
        console.log(`发布 ${
      
      test} 的结果:`);
        // 发布试卷
        Observer.fire(test)
    }
};
const s1 = new Student('Tom');
const s2 = new Student('Lee');
const s3 = new Student('张三');

// s1 考了试卷1、2,最终又放弃了试卷2
s1.setResult('试卷1', 10);
s1.setResult('试卷2', 20);
s1.giveUp('试卷2');

// s2 考了试卷2、3,放弃了试卷1
s2.giveUp('试卷1');
s2.setResult('试卷2', 80);
s2.setResult('试卷3', 100);

// s3 考了试卷1、2、3
s3.setResult('试卷1', 70);
s3.setResult('试卷2', 50);
s3.setResult('试卷3', 30);

console.log('=============');

const teacher = new Teacher();

teacher.distributeTest('试卷1');
console.log('=============');

teacher.distributeTest('试卷2');
console.log('=============');

teacher.distributeTest('试卷3');
console.log('=============');

console.log(Observer);

超级玛丽-状态模式

状态模式(State)

当一个对象的内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对象。

主要目的就是将条件判断的不同结果转化为状态对象的内部状态。

解决多分支判断问题

  • 一般判断形势 (当新增需求时那么就需要添加新的判断条件)
    // 展示结果
    function showResult(state) {
          
          
        if (state == 0) {
          
          
            // 处理结果0
            console.log('这是第一种情况');
        } else if (state == 1) {
          
          
            // 处理结果1
            console.log('这是第二种情况');
        } else if (state == 2) {
          
          
            // 处理结果2
            console.log('这是第三种情况');
        } else if (state == 3) {
          
          
            // 处理结果3
            console.log('这是第四种情况');
        }
    }
    
    showResult(2); // 这是第三种情况
    
  • 状态对象的实现 (状态模式的基本雏形)
    // 投票结果状态对象
    const ResutlState = (function () {
          
          
        // 判断结果保存在内部状态中
        var State = {
          
          
            // 每种状态作为一种独立方法保存
            state0: function () {
          
          
                // 处理结果0
                console.log('这是第一种情况');
            },
            state1: function () {
          
          
                // 处理结果1
                console.log('这是第二种情况');
            },
            state2: function () {
          
          
                // 处理结果2
                console.log('这是第三种情况');
            },
            state3: function () {
          
          
                // 处理结果3
                console.log('这是第四种情况');
            }
        };
        // 获取某一种状态并执行其对应的方法
        function show(type) {
          
          
            State[`state${
            
            type}`] && State[`state${
            
            type}`]();
        }
        return {
          
          
            // 返回调用状态方法接口
            show
        }
    }());
    
    ResutlState.show(2); // 这是第三种情况
    

超级玛丽示例:

  • if ... else ...形势判断角色动作:
    // 单动作条件判断 每增加一个动作就需要添加一个判断
    function playerController(state) {
          
          
        if (state === 'move') {
          
          
            console.log('移动');
        } else if (state === 'jump') {
          
          
            console.log('跳跃');
        } else {
          
          
            console.log('idle');
        }
    }
    // 复合动作对条件判断的开销是翻倍的
    function playerController(state1, state2) {
          
          
        if (state1 === 'move') {
          
          
            console.log('移动');
        } else if (state1 === 'move' && state2 === 'shoot') {
          
          
            console.log('移动射击');
        } else if (state1 === 'jump') {
          
          
            console.log('跳跃');
        } else if (state1 === 'jump' && state2 === 'shoot') {
          
          
            console.log('跳跃射击');
        } else if (state1 === 'idle' && state2 === 'shoot') {
          
          
            console.log('站立射击');
        } else {
          
          
            console.log('idle');
        }
    }
    
  • 状态优化形势:
    // 创建超级玛丽状态类
    const Player = function () {
          
          
    
        // 内部状态私有变量(缓存当前状态)
        let _currentState = [];
    
        // 动作与状态方法映射
        const state = {
          
          
            // 跳跃
            jump() {
          
           console.log('jump'); },
            // 移动
            move() {
          
           console.log('move'); },
            // 射击
            shoot() {
          
           console.log('shoot'); },
            // 蹲下
            squat() {
          
           console.log('squat'); }
        };
    
        // 动作控制类,返回接口方法 changeState、action
        return {
          
          
            // 改变状态方法 - 组合动作通过传递多个参数实现
            changeState(...args) {
          
          
                // 重置并向内部状态中添加动作
                _currentState = args;
                // 返回动作控制类
                return this;
            },
            // 执行动作
            action() {
          
          
                console.log('触发一次动作');
                // 遍历内部状态保存的动作 - 如果该动作存在则执行
                _currentState.forEach(key => state[key] && state[key]())
                return this;
            }
        };
    };
    
    // 创建一个超级玛丽
    const player = new Player();
    player
        .changeState('jump', 'shoot')       // 改变为`跳跃`与`射击`动作组合
        .action()                           // 执行动作             触发一次动作 ---> jump - shoot
        .action()                           // 执行动作             触发一次动作 ---> jump - shoot
        .changeState('shoot')               // 改变为`射击`动作
        .action();                          // 执行动作             触发一次动作 ---> shoot
    

特点:

状态模式既是 解决程序中臃肿的分支判断语句问题,将 每个分支转化为一种 状态 独立出来方便 每种 状态管理不至于每次执行时遍历所有分支

活诸葛-策略模式

策略模式(Strategy)

将定义的一组算法封装起来,使其相互之间可以替换。封装的算法具有一定独立性,不会随客户端变化而变化。

策略对象(需求:商场促销活动)

这种模式我们不需要关注内部的算法是什么,只需要关注最终返回的结果即可,类似状态模式

// 价格策略对象
var PriceStrategy = (function () {
    
    
    // 内部算法对象
    var stragtegy = {
    
    
        // 100 返 30
        return30(price) {
    
    
            // parseInt可通过~~、|等运算符替换,要注意此时price要在[-2147483648, 
            // 2147483647]之间
            // +price 转化为数字类型
            return +price + parseInt(price / 100) * 30;
        },
        // 100 返 50
        return50(price) {
    
    
            return +price + parseInt(price / 100) * 50;
        },
        // 9 折
        percent90(price) {
    
    
            // JavaScript 在处理小数乘除法有bug,故运算前转化为整数
            return price * 100 * 90 / 10000;
        },
        // 8 折
        parcent80(price) {
    
    
            return price * 100 * 80 / 10000;
        },
        // 5 折
        percent50(price) {
    
    
            return price * 100 * 50 / 10000;
        }
    }
    // 策略算法调用接口
    return function (algorithm, price) {
    
    
        // 如果算法存在,则调用算法,否则返回false
        return stragtegy[algorithm] && stragtegy[algorithm](price)
    }
}());

let price = PriceStrategy('percent50', 100);
console.log(price); // 50

缓冲函数jq中的策略模式

在这里插入图片描述

<div class="box1" style="height: 100px;background-color: red;"></div>
    <div class="box2" style="height: 100px;background-color: greenyellow"></div>
    <div class="box2" style="height: 100px;background-color: blue;"></div>
    <div class="box2" style="height: 100px;background-color: yellow;"></div>
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script>
<script>  
    $('.box1').animate({
      
       width: '200px' }, 5000, 'linear');
    $('.box2').animate({
      
       width: '200px' }, 5000, 'swing');
    $('.box3').animate({
      
       width: '200px' }, 5000, 'easeOutQuart');
    $('.box4').animate({
      
       width: '200px' }, 5000, 'easeOutBounce');
</script>

表单验证

策略模式的表单示例

// 表单正则验证策略对象
var InputStrategy = (function () {
    
    
    var strategy = {
    
    
        // 是否为空
        notNull(value) {
    
    
            return /\s+/.test(value) ? '请输入内容' : '正确';
        },
        // 是否是一个数字
        number(value) {
    
    
            return /^[0-9]+(\.[0-9]+)?$/.test(value) ? '正确' : '请输入数字';
        },
        // 是否是本地电话
        phone(value) {
    
    
            return /^\d{3}\-\d{8}$|^\d{4}\-\d{7}$/.test(value) ? '正确' : `请输入正
            确的电话号码格式, 如: 010-12345678 或 0418-1234567`;
        }
    }
    return {
    
    
        // 验证接口 type 算法 value 表单值
        check(type, value) {
    
    
            // 去除收尾空白符
            value = value.replace(/^\s+|\s+$/g, '');
            return strategy[type] ? strategy[type](value) : '没有该类型的检测方法'
        },
        // 添加策略
        addStrategy(type, fn) {
    
    
            strategy[type] = fn;
        }
    }
}());

InputStrategy.check('number', '1234');  // '正确'
InputStrategy.check('number', '123a4'); // '请输入数字'

InputStrategy.check('username', 'Lee'); // '没有该类型的检测方法'
InputStrategy.addStrategy('username', function (value) {
    
    
    return value.length <= 3 ? '正确' : '用户名不得大于3个字符';
});
InputStrategy.check('username', 'Lee'); // '正确'
InputStrategy.check('username', 'PLee'); // '用户名不得大于3个字符'

特点:

  • 策略模式优点
    1. 策略模式封装了一组代码簇,并且封装的代码相互之间独立,便于对算法的重复引用提高了算法的复用率
    2. 策略模式继承相比,在类的继承当需求很多算法时,就不得不创建出多种类,这样会导致算法与算法的使用者耦合在一起不利于算法的独立演化,并且在类的外部改变类的算法难度也是极大的
    3. 同状态模式一样,策略模式也是一种优化分支判断语句的模式,采用策略模式 对算法封装使得算法利于维护
  • 策略模式缺点
    • 由于选择哪种算法的决定权在用户,所以对用户来说就必须了解每种算法的实现。这就增加了用户对策略对象的使用成本。其次,由于每种算法间相互独立,这样对于一些复杂的算法处理相同逻辑的部分无法实现共享,这就会造成一些资源的浪费。当然你可以通过享元模式来解决

有序车站-职责链模式

职责链模式(Chain of Responsibility)

解决请求的发送者与请求的接受者之间的耦合,通过职责链上的多个对象对分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理。

分解需求

把一个半成品的需求分解为要基于那些功能模块去实现,各自负责各自的职责,提高实现需求的颗粒度

例如一个请求渲染的需求

分模块细化颗粒度

// data.json
{
    
    
    "code": 200,
    "data": {
    
    
        "name": "Lee",
        "age": 18
    },
    "msg": "ok"
}

第一站 —— 请求模块

/**
 * 异步请求对象(简化版本)
 * @param {string} url 请求地址
 * @param {string} type 请求类型
 * @param {object} data 请求数据
 */
const request = function (url, type, data) {
    
    
    // XHR对象 简化版本 IE另行处理
    var xhr = new XMLHttpRequest();
    // 请求返回事件
    xhr.onload = function (event) {
    
    
        // 请求成功
        if (xhr.status === 200) {
    
    
            console.log(xhr);
            success(xhr.response);
        }
        // 请求失败
        else {
    
    
            fail(xhr.statusText);
        }
    };
    // 拼接请求字符串
    url += '?';
    for (let key in data) {
    
    
        url += key + '=' + data[key] + '&';
    }
    // 发送异步请求
    xhr.open(type, url, true);
    xhr.send(null);
};

下一站 —— 响应数据适配模块

/**
 * 成功的回调
 * @param {object} res 
 */
function success(res) {
    
    
    let {
    
     data } = JSON.parse(res);
    render(data);
}

/**
 * 失败的回调
 * @param {string} err 
 */
function fail(err) {
    
    
    console.log(err);
}

终点站 —— 创建组件模块

/**
 * 渲染数据 `<label>Lee: </label><span>18</span>`
 * @param {object} data 响应数据 
 */
function render(data) {
    
    
    const label = document.createElement('label');
    label.innerText = data.name + ': ';
    const span = document.createElement('span');
    span.innerText = data.age;
    document.body.appendChild(label);
    document.body.appendChild(span);
}

request('./data.json', 'GET', {
    
     id: 'abc' });

单元测试

解决目前请求接口地址未提供问题

success(JSON.stringify({
    
    
    "code": 200,
    "data": {
    
    
        "name": "Tom",
        "age": 24
    },
    "msg": "ok"
}));

命令模式

命令模式(Command)

将请求与实现解耦并封装成独立对象,从而使不同的请求对客户端的实现参数化。

自由化创建视图命令

在这里插入图片描述

<style>
    #title-box .title .main h2{
      
      
        text-align: center;
    }
    #title-box .title .main p{
      
      
        text-align: right;
    }
    #picture-box {
      
      
        text-align: center;
    }
    #picture-box .picture{
      
      
        position: relative;
        display: inline-block;
        margin: 10px;
        border: 1px solid #333;
    }
    #picture-box .picture p{
      
      
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        height: 30px;
        line-height: 30px;
        text-align: center;
        margin: 0;
        background-color: rgba(0, 0, 0, .5);
        color: #fff;
    }
</style>
<body>
    <div id="title-box"></div>
    <div id="picture-box"></div>
</body>
// 模块实现模块
var viewCommand = (function () {
    
    

    // 模板
    let tpl = {
    
    
        // 标题结构模板
        title:
            `<div class="title">
                <div class="main">
                    <h2>{#title#}</h2>
                    <p>{#tips#}</p>
                </div>
            </div>`,
        // 图片结构模板
        picture:
            `<div class="picture">
                <img src="{#src#}"/>
                <p>{#text#}</p>
            </div>`,
    };

    // 存储经过格式化最终返回的html
    let html = '';

    /**
     * 格式化字符串将
     * @param {string} str 原字符串 
     * @param {object} obj 模板数据
     * @returns 最终解析好的html字符串
     */
    function formateString(str, data) {
    
    
        return str.replace(/\{#(\w+)#\}/g, function (_match, key) {
    
    
            return data[key];
        })
    }
    // html = formateString(tpl.title, {title: 'You瞧谁不起 - 道不尽世间的沧桑,诉不完人生的悲凉', tips: '人海茫茫,你我依旧孤独~~~'});
    // document.body.innerHTML = html;
    // console.log(html);

    // 定义指令
    let Action = {
    
    
        /**
         * 解析数据方法html变量中
         * @param {string} view 模板视图
         * @param {object} data 数据
         */
        create(view, data) {
    
    
            // 针对数组的模板解析d
            if (Array.isArray(data)) {
    
    
                data.forEach(d => {
    
    
                    html += formateString(tpl[view], d);
                })
            }
            // 针对对象的模板解析
            else {
    
    
                html += formateString(tpl[view], data);
            }
        },
        /**
         * 展示html到指定容器内,如果此时定义了模板数据解析那么追加模板
         * @param {string} id DOM ID
         * @param {string} view 模板视图(追加)
         * @param {object} data 数据
         */
        display(id, view, data) {
    
    
            console.log(id, view, data);
            if (data) {
    
    
                this.create(view, data); // 追加模板
            }
            console.log(html);
            document.getElementById(id).innerHTML = html;
            html = ''; // 展示后清空缓存的字符串
        }
    };

    /**
     * 命令接口
     * @param {object} param
     *  {
     *      @param {Array} params   数组参数 create->[view, data] display->[id, view, data]
     *      @param {string} command 指令-create/display
     *  }
     */
    return function excute({
     
      params, command }) {
    
    
        // 将参数转转换未数组
        params = Array.isArray(params) ? params : [params];
        // 执行指令
        Action[command].apply(Action, params);
    }
})();

const objData = {
    
    
    title: 'You瞧谁不起 - 道不尽世间的沧桑,诉不完人生的悲凉',
    tips: '人海茫茫,你我依旧孤独~~~'
};

// 渲染标题模块DOM
viewCommand({
    
    
    command: 'display',
    params: ['title-box', 'title', objData]
});


const listData1 = [
    {
    
     text: '图片01', src: 'https://dummyimage.com/200x100/ff0/fff' },
    {
    
     text: '图片02', src: 'https://dummyimage.com/200x100/0ff/fff' },
    {
    
     text: '图片03', src: 'https://dummyimage.com/200x100/0f0/fff' },
];

const listData2 = [
    {
    
     text: '图片04', src: 'https://dummyimage.com/200x100/f0f/fff' },
    {
    
     text: '图片05', src: 'https://dummyimage.com/200x100/00f/fff' },
    {
    
     text: '图片06', src: 'https://dummyimage.com/200x100/f00/fff' },
];

// 暂存使用listData1解析 出来的html字符串
viewCommand({
    
    
    command: 'create',
    params: ['picture', listData1]
});

// 渲染 暂存的 和 使用listData2解析 出来的html字符串
viewCommand({
    
    
    command: 'display',
    params: ['picture-box', 'picture', listData2]
});

绘图命令

在这里插入图片描述

// 绘图指令
var CanvasCommand = (function () {
    
    
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');

    // 指令方法
    var Action = {
    
    
        /**
         * 填充色彩
         * @param {string} c 颜色
         */
        fillStyle(c) {
    
    
            ctx.fillStyle = c;
        },

        /**
         * 填充矩形
         * @param {number} x 位置x
         * @param {number} y 位置y
         * @param {number} w 矩形宽度
         * @param {number} h 矩形高度
         */
        fillRect(x, y, w, h) {
    
    
            ctx.fillRect(x, y, w, h);
        },

        /**
         * 描边色彩
         * @param {string} c 颜色
         */
        strokeStyle(c) {
    
    
            ctx.strokeStyle = c;
        },

        /**
         * 描边矩形
         * @param {number} x 位置x
         * @param {number} y 位置y
         * @param {number} w 矩形宽度
         * @param {number} h 矩形高度
         */
        strokeRect(x, y, width, height) {
    
    
            ctx.strokeRect(x, y, width, height);
        },

        /** ...CODE... */
    };

    // 命令接口
    return function (data) {
    
    
        data = Array.isArray(data) ? data : [data];
        data.forEach(d => {
    
    
            let params = Array.isArray(d.params) ? d.params : [d.params];
            Action[d.command].apply(Action, params);
        })
    }
})();

// 执行多个指令
CanvasCommand([
    {
    
     command: 'fillStyle', params: 'red' },
    {
    
     command: 'fillRect', params: [20, 20, 100, 100] }
]);

// 执行单个指令
CanvasCommand({
    
    
    command: 'fillRect',
    params: [150, 20, 50, 50],
});

特点:

不用关心每种方法的具体实现了,即使在不同浏览器实现不一致我们也不用关心,因为那些不兼容的问题都已经在命令对象内部实现了

命令模式是将执行的命令封装,解决命令的发起者命令的执行者之间的耦合

每条命令实质上是一个操作。命令的 使用者 不必了解 命令的执行者(命令对象)的命令接口如何实现的、命令是如何接受的、命令是如何执行的。所有的命令都被存储在命令对象中

  • 优点
    • 解决命令使用者之间的耦合;
    • 新的命令很容易加入到命令系统中,供使用者使用;
    • 命令的使用具有一致性,多数的命令在一定程度上是简化操作方法的使用的;
  • 缺点
    • 命令模式是对一些操作的封装,这就造成每执行一次操作都要调用一次命令对象,增加了系统的复杂度;

驻华大使-访问者模式

访问者模式(Visitor)

针对于对象结构中的元素,定义在不改变该对象的前提下访问结构中元素的新方法。

解决this指向在IE下指向window的问题:

  • 错误示例:(IE8及以下this指向window
    /**
     * 绑定事件
     * @param {HTMLElement} el DOM元素
     * @param {string} type 事件类型 
     * @param {Function} fn 时间函数
     */
    function bindEvent(el, type, fn) {
          
          
        if (el.addEventListener) {
          
          
            el.addEventListener(type, fn, false);
        } else if (el.attachEvent) {
          
          
            el.attachEvent('on' + type, fn);
        } else {
          
          
            el['on' + type] = fn;
        }
    }
    
    var text = document.getElementById('text');
    
    bindEvent(text, 'click', function () {
          
          
        // 非IE下:     HTMLElement         false
        // IE8及以下:  [object Window]     true
        console.log(this, this === window);
    
        this.style.backgroundColor = 'red'; // IE8及以下报错
    });
    
  • 解决IE8及以下报错问题:
    /**
     * 绑定事件
     * @param {HTMLElement} el DOM元素
     * @param {string} type 事件类型 
     * @param {Function} fn 时间函数
     */
    function bindEvent(el, type, fn) {
          
          
        if (el.addEventListener) {
          
          
            el.addEventListener(type, fn, false);
        } else if (el.attachEvent) {
          
          
            // 解决IE8及以下报错问题
            el.attachEvent('on' + type, function (e) {
          
          
                fn.call(el, e); // 修改了回调函数的指向
            });
        } else {
          
          
            el['on' + type] = fn;
        }
    }
    
    var text = document.getElementById('text');
    
    bindEvent(text, 'click', function () {
          
          
        console.log(this); // 指向了HTMLElement
        this.style.backgroundColor = 'red';
    });
    

为对象变量添加类似数组的方法

  • 数组操作方法(pop splice push):
    let b = [];
    b.name = 'Lee';
    b.age = 18;
    console.log(b.push(1, 2, 3, 4));            // 4
    console.log(b);                             // [1, 2, 3, 4, name: 'Lee', age: 18] ---> length: 4
    console.log(b.splice(1, 2));                // (2) [2, 3]
    console.log(b);                             // (2) [1, 4, name: 'Lee', age: 18] ---> length: 2
    console.log(b.pop());                       // 4
    console.log(b);                             // [1, name: 'Lee', age: 18] ---> length: 1
    
  • 利用访问者模式实现对象变量访问数组上自带的事件方法:
    // 访问器
    var Visitor = (function () {
          
          
        return {
          
          
            // 追加数据方法
            push(...args) {
          
          
                // 对第一个参数对象执行push方法
                return Array.prototype.push.apply(args[0], args.slice(1));
            },
            // 截取方法
            splice(...args) {
          
          
                // 对第一个参数对象执行splice方法
                return Array.prototype.splice.apply(args[0], args.slice(1));
            },
            // 弹出最后一次添加的元素
            pop(...args) {
          
          
                // 对第一个参数对象执行pop方法
                return Array.prototype.pop.apply(args[0]);
            }
        }
    })();
    
    let a = {
          
           name: 'Lee', age: 18 };
    
    console.log(Visitor.push(a, 1, 2, 3, 4));   // 4
    console.log(a);                             // {0: 1, 1: 2, 2: 3, 3: 4, name: 'Lee', age: 18, length: 4}
    
    console.log(Visitor.splice(a, 1, 2));       // (2) [2, 3]
    console.log(a);                             // {0: 1, 1: 4, name: 'Lee', age: 18, length: 2}
    
    console.log(Visitor.pop(a));                // 4
    console.log(a);                             // {0: 1, name: 'Lee', age: 18, length: 1}
    

特点:

访问者更适合于那些数据稳定,但是数据的操作方法易变的环境下。因此当操作环境改变时,可以自由修改操作方法以适应操作环境,而不用修改原数据实现操作方法的拓展

同时对于同一个数据,它可以被多个访问对象所访问,这极大增加了操作数据的灵活性

访问者模式指在不同的环境下都可以从容的进行事件调用及访问,以及使用对象去访问数组可以直接访问的方法。

媒婆-中介者模式

中介者模式(Mediator)

通过中介者对象封装一系列对象之间的交互,使对象之间不再相互引用,降低他们之间的耦合。有时中介者对象也可改变对象之间的交互。

需求:为导航模块添加设置层用来控制显示隐藏

在这里插入图片描述

<style>
    .icon {
      
      
        width: 30px;
        height: 30px;
        fill: currentColor;
        overflow: hidden;
    }

    .nav {
      
      
        width: 300px;
        height: 40px;
        padding-top: 10px;
        background-color: #fafafa;
        display: flex;
        justify-content: space-between;
        align-items: center;
        border: 1px solid #aaa;
    }

    .nav .nav-item {
      
      
        flex: 1;
        text-align: center;
    }

    .nav .nav-item span {
      
      
        cursor: pointer;
        position: relative;
        display: inline-block;
    }

    .nav .nav-item span b {
      
      
        position: absolute;
        top: -5px;
        right: -10px;
        font-size: 8px;
        padding: 1px 3px;
        background-color: red;
        color: #fff;
        border-radius: 10px;
    }
</style>

<body>
    <label for="hide_nav"><input id="hide_nav" checked type="checkbox" />显示/隐藏导航</label>
    <label for="hide_notice"><input id="hide_notice" checked type="checkbox" />显示/隐藏通知</label>
    <nav class="nav">
        <div id="home_nav" class="nav-item">
            <span>
                <svg class="icon" aria-hidden="true">
                    <use xlink:href="#icon-fangzi"></use>
                </svg>
                <b id="home_notice">10</b>
            </span>
        </div>
        <div id="community_nav" class="nav-item">
            <span>
                <svg class="icon" aria-hidden="true">
                    <use xlink:href="#icon-shequ"></use>
                </svg>
                <b id="community_notice">99+</b>
            </span>
        </div>
        <div id="me_nav" class="nav-item">
            <span>
                <svg class="icon" aria-hidden="true">
                    <use xlink:href="#icon-wode"></use>
                </svg>
                <b id="me_notice">24</b>
            </span>
        </div>
    </nav>
    <script src="./icons/iconfont.js"></script>
    <script src="./index.js"></script>
</body>
// 中介者对象
var Mediator = (function () {
    
    
    // 消息对象
    var _messages = {
    
    };
    return {
    
    
        _messages,
        /**
         * 订阅消息方法
         * @param {string} type 消息名称
         * @param {Function} action 消息回调函数
         */
        register(type, action) {
    
    
            // 如果该消息不存在,则建立该消息容器
            if (!_messages[type]) {
    
    
                _messages[type] = [];
            }
            // 存入新消息回调函数
            _messages[type].push(action);
        },
        /**
         * 发布消息方法
         * @param {*} type 消息名称
         */
        send(type) {
    
    
            // 如果该消息已经被订阅
            if (_messages[type]) {
    
    
                // 遍历已存储的消息回调函数
                _messages[type].forEach(action => action && action());
            }
        }
    };
}());

/**
 * 显示隐藏组件
 * @param {string[]} ids DOM元素
 * @param {boolean} isShow 显示隐藏
 */
const isShowNavWidget = function (ids, isShow) {
    
    
    ids.forEach(id => {
    
    
        const widget = document.getElementById(id);
        widget.style.visibility = isShow ? 'visible' : 'hidden';
    })
};

// 注册组件显示隐藏
(function () {
    
    
    // 显示导航 --- 不需要显示通知
    Mediator.register('showNav', function () {
    
    
        isShowNavWidget(['home_nav', 'community_nav', 'me_nav'], true);
    });

    // 隐藏导航 --- 必须隐藏通知
    Mediator.register('hideNav', function () {
    
    
        isShowNavWidget(['home_nav', 'community_nav', 'me_nav'], false);
    });
    Mediator.register('hideNav', function () {
    
    
        isShowNavWidget(['home_notice', 'community_notice', 'me_notice'], false);
    });

    // 显示通知 --- 必须显示导航
    Mediator.register('showNotice', function () {
    
    
        isShowNavWidget(['home_notice', 'community_notice', 'me_notice'], true);
    });
    Mediator.register('showNotice', function () {
    
    
        isShowNavWidget(['home_nav', 'community_nav', 'me_nav'], true);
    });

    // 隐藏通知 --- 不需要隐藏导航
    Mediator.register('hideNotice', function () {
    
    
        isShowNavWidget(['home_notice', 'community_notice', 'me_notice'], false);
    });
}());

// 设置层模块
(function () {
    
    
    const hideNav = document.getElementById('hide_nav');
    const hideNotice = document.getElementById('hide_notice');

    // 显示隐藏所有导航
    hideNav.onchange = function (e) {
    
    
        // 显示导航 --- 不需要显示通知
        if (hideNav.checked) {
    
    
            Mediator.send('showNav');
        }
        // 隐藏导航 --- 必须隐藏通知
        else {
    
    
            hideNotice.checked = false;
            Mediator.send('hideNav');
        }
    };
    // 显示隐藏所有通知
    hideNotice.onchange = function (e) {
    
    
        // 显示通知 --- 必须显示导航
        if (hideNotice.checked) {
    
    
            hideNav.checked = true;
            Mediator.send('showNotice');
        }
        // 隐藏通知 --- 不需要隐藏导航
        else {
    
    
            Mediator.send('hideNotice');
        }
    };
})();

console.log(Mediator);

特点:

  • 观察者模式相比
    • 观察者模式一样,中介者模式的主要业务也是通过模块间或者对象间的复杂通信,来解决模块间或对象间的耦合。
    • 中介者对象的本质是分装多个对象的交互,并且这些对象的交互一般都是在中介者内部实现的。
    • 观察者模式中,订阅者是双向的,既可以是消息的发布者,也可以是消息的订阅者。
    • 中介者模式中,订阅者是单向的,只能是消息的订阅者。而消息统一由中介者对象发布,所有的订阅者对象间接地被中介者管理。
  • 外观模式封装特性相比
    • 外观模式封装的目的是为了提供更简单的易用接口,而不会添加其他功能。
    • 中介者模式对多个对象交互地封装,且这些对象一般处于同一层面上,并且封装的交互在中介者内部。

做好笔记-备忘录模式

备忘录模式(Memento)

在不破坏对象的封装性的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态。(缓存数据方便后续直接使用)

需求:避免每次都重复调用接口获取数据

在这里插入图片描述

<button id="search_btn">获取数据</button>
<div id="content"></div>
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script>
// 备忘录模式(缓存数据)
const Page = (function () {
    
    
    // 定义备忘录(缓存对象)
    let cache = {
    
    };
    return function (key, fn) {
    
    
        if (cache[key]) {
    
    
            fn && fn(cache[key]);
        } else {
    
    
            $.get('./data.json', function (res) {
    
    
                if (res.code === 200) {
    
    
                    cache[key] = res.data.list;
                    fn && fn(cache[key]);
                    console.log(cache);
                }
            })
        }
    }
}());

// 点击请求接口
$('#search_btn').click(function (e) {
    
    
    Page('tableData', function (list) {
    
    
        const html =
            `<table border>
                <tr><th>姓名</th><th>年龄</th></tr>
                ${
      
      list.map(d => `<tr><td>${ 
        d.name}</td><td>${ 
        d.age}</td></tr>`).join('')}
            </table>`;
        $('#content').html(html);
    });
});

点钞机-迭代器模式

迭代器模式(Iterator)

在不暴露对象内部结构的同时,可以顺序地访问聚合对象内部的元素。

迭代器:解决处理多个元素不同处理的问题,简化遍历操作

比如我们拥有一系列的DOM元素,而需求是处理其中的一部分元素,而不是全部。一般方法我们会去遍历所有DOM元素,然后判断指定元素实现我们指定的方法,这么做就冗余遍历了我们不需要处理的元素,而迭代器模式很好的解决了这一问题。

迭代器可以顺序地访问一个聚合对象内部的元素的,它可以简化遍历操作,就像银行里的点钞机,有了它降低了我们点钞成本,安全而可靠。

/**
 * 获取DOM元素
 * @param {string} id DOMID
 * @returns DOM元素
 */
function $(id) {
    
    
    return document.getElementById(id);
}

/**
 * 迭代器
 * @param {string} containerId 父DOM元素ID
 * @param {string} tagName 子DOM元素名称
 * @returns 对象,内存定义有针对元素的操作
 */
const Iterator = function (containerId, tagName) {
    
    
    const container = $(containerId);
    const tags = container.getElementsByTagName(tagName);
    const len = tags.length;
    let index = 0;
    return {
    
    
        /**
         * 获取第一个DOM元素
         * @returns DOM元素
         */
        first() {
    
    
            index = 0;
            return tags[index];
        },
        /**
         * 获取最后一个DOM元素
         * @returns DOM元素
         */
        last() {
    
    
            index = len - 1;
            return tags[index];
        },
        /**
         * 获取当前DOM元素
         * @param {number} i 索引
         * @returns DOM元素
         */
        get(i) {
    
    
            // 如果num大于等于0再获取正向获取;当i>=len时i继续从0开始索引;小于0时,逆向获取
            index = i >= 0 ? (i % len) : Math.abs(len + i % len);
            return tags[index];
        },
        /**
         * 获取前一个DOM元素
         * @returns DOM元素
         */
        pre() {
    
    
            return this.get(--index);
        },
        /**
         * 获取后一个DOM元素
         * @returns DOM元素
         */
        next() {
    
    
            return this.get(++index);
        },
        /**
         * 对每一个元素执行某一个方法
         * @param {Function} fn 回调函数-this指向当前元素
         * @param  {...any} args 传入回调的参数
         */
        dealEach(fn, ...args) {
    
    
            [...tags].forEach(tag => {
    
    
                fn.apply(tag, args);
            });
        },
        /**
         * 对某一个元素执行某一个方法
         * @param {*} i 索引
         * @param {*} fn 回调函数-this指向当前元素
         * @param  {...any} args 传入回调的参数
         */
        dealItem(i, fn, ...args) {
    
    
            fn.apply(this.get(i), args);
        },
        /**
         * 对 所有元素 以及 对 某个或某些元素 的处理 
         * @param {number|number[]} i 索引
         * @param {Function} allFn 对每一个元素执行某一个方法的回调
         * @param {Function} iFn 对某一个元素执行某一个方法的回调
         */
        exclusive(i, allFn, iFn) {
    
    
            this.dealEach(allFn);
            if (Array.isArray(i)) {
    
    
                i.forEach(j => this.dealItem(j, iFn));
            } else {
    
    
                this.dealItem(i, iFn);
            }
        }
    }
};

在这里插入图片描述

<ol id="container1">
    <li>Lee</li>
    <li>Tom</li>
    <li>Lucy</li>
    <li>张三</li>
    <li>李四</li>
    <li>王五</li>
    <li>赵六</li>
    <li>哈哈</li>
    <li>呵呵</li>
    <li>嘿嘿</li>
    <li>嘻嘻</li>
</ol>
const it1 = Iterator('container1', 'li');
console.log(it1);
console.log(it1.first());    // <li>Lee</li>
console.log(it1.last());     // <li>嘻嘻</li>
console.log(it1.get(-2));    // <li>嘿嘿</li>
console.log(it1.pre());      // <li>呵呵</li>
console.log(it1.next());     // <li>嘿嘿</li>
it1.dealEach(function (color) {
    
    
    this.style.color = color;
}, '#ff088f');
it1.dealItem(7, function (text, color) {
    
    
    this.innerText += text;
    this.style.color = color;
}, '-疯子', 'blue');

在这里插入图片描述

<ol id="container2">
    <li>Tom</li>
    <li>张三</li>
    <li>李四</li>
    <li>王五</li>
    <li>赵六</li>
    <li>哈哈</li>
    <li>呵呵</li>
    <li>嘿嘿</li>
    <li>嘻嘻</li>
</ol>
const it2 = Iterator('container2', 'li');
console.log(it2);
it2.exclusive([-1, 3, 5, 12], function () {
    
    
    this.innerText += ' - [全部元素的处理]';
    this.style.backgroundColor = 'red';
}, function () {
    
    
    this.innerText += ' -【指定元素的处理】';
    this.style.backgroundColor = 'green';
});

数组迭代器

/**
 * 仿forEach
 * @param {any[]} arr 数组
 * @param {Function} fn 回调
 */
const eachArray = function (arr, fn) {
    
    
    for (let i = 0; i < arr.length; i++) {
    
    
        if (fn.call(arr[i], arr[i], i) === false) break;
    }
};

eachArray(['Lee', 'Tom', 'Lucy'], function (item, i) {
    
    
    console.log(item, i); // 'Lee' --> 0 | 'Tom' --> 1 | 'Lucy' --> 2
    // if(item === 'Tom') return false; // 'Lee' --> 0 | 'Tom' --> 1 
});

对象迭代器

/**
 * 仿forEach
 * @param {object} obj 对象
 * @param {Function} fn 回调
 */
const eachObject = function (obj, fn) {
    
    
    for (const key in obj) {
    
    
        if (Object.hasOwnProperty.call(obj, key)) {
    
    
            if (fn.call(obj[key], key, obj[key]) === false) break;
        }
    }
};

eachObject({
    
    name: 'Lee', age: 18, desc: '呜呜~~~'}, function (key, value) {
    
    
    /**
     * name --->    Lee
     * age  --->    18
     * desc --->    呜呜~~~
     */
    console.log(key, value);
});

同步变量迭代器

解决a.b.c.d报错问题

Uncaught TypeError: Cannot read properties of undefined (reading 'c')

// 同步变量
let A = {
    
    
    code: 200,
    data: {
    
    
        name: 'Lee',
        age: 18,
        info: {
    
    
            desc: '描述一下呗',
            lang: 'zh-cn'
        }
    },
    msg: 'ok'
};

/**
 * 同步变量迭代取值器
 * @param {string} key key字符串
 */
const AGetter = function (key) {
    
    
    if (!A) return false;
    let result = A;
    const keys = key.split('.');
    for (let i = 0; i < keys.length; i++) {
    
    
        let k = keys[i];
        result = result[k] || undefined;
        if (result === undefined) return undefined;
    }
    return result;
};

// undefined
AGetter('data.name.age.xxx');
// '描述一下呗'
AGetter('data.info.desc');
// {name: 'Lee', age: 18, info: {desc: '描述一下呗', lang: 'zh-cn'}}
AGetter('data');

/**
 * 同步变量迭代赋值器
 * @param {string} key key字符串
 * @param {any} val 值
 */
const ASetter = function (key, val) {
    
    
    if (!A) return false;
    let result = A;
    const keys = key.split('.');
    let len = keys.length;
    for (let i = 0; i < len - 1; i++) {
    
    
        let k = keys[i];
        result[k] = result[k] || {
    
    };
        result = result[k];
    }
    return result[keys[len - 1]] = val;
};

ASetter('data.aaa.bbb.ccc', 'ABC');

/**
 * {
 *      "code":200,
 *      "data":{
 *          "name":"Lee",
 *          "age":18,
 *          "info":{
 *              "desc":"描述一下呗",
 *              "lang":"zh-cn"
 *          },
 *          "aaa":{
 *              "bbb":{
 *                  "ccc":"ABC"
 *              }
 *          }
 *      },
 *      "msg":"ok"
 * }
 */
console.log(A);

分支循环嵌套问题

处理图像

在这里插入图片描述

/**
 * 获取DOM元素
 * @param {string} id DOMID
 * @returns DOM元素
 */
function $(id) {
    
    
    return document.getElementById(id);
}

// 获取canvas
const canvas = $('canvas');
const ctx = canvas.getContext('2d');

// 定义图片
const image = new Image();
image.src = './00.png';
const scale = 0.3;
const [w, h] = [image.width * scale, image.height * scale];
// [image.width, image.height] = [w, h];
// document.body.append(image);

// 配置canvas宽高
[canvas.width, canvas.height] = [w * 3, h];

// 图片加载完成
image.onload = function () {
    
    
    ctx.drawImage(image, 0, 0, w, h);
    dealImage(ctx, 'gray', 0, 0, w, h, 255);
    dealImage(ctx, 'red', w, 0, w, h, 255);
};

将循环遍历抽象出来作为一个迭代器存在,每次循环都执行传入迭代器中的某一固定算法,而对于特效算法我们可以设置在策略对象中实现,通过策略模式与迭代器模式的综合运用即可解决上述分支判断问题。代码如下:

/**
 * 绘制特效图片
 * @param {CanvasRenderingContext2D} ctx 绘制对象
 * @param {string} effect 特效标识 
 * @param {number} x 位置x
 * @param {number} y 位置y
 * @param {number} w 宽度
 * @param {number} h 高度
 * @param {number} opacity 透明度
 */
const dealImage = function (ctx, effect, x, y, w, h, opacity) {
    
    
    const imageData = ctx.getImageData(x, y, w, h);
    let {
    
     data } = imageData;

    // 状态模式封装算法
    const Deal = (function () {
    
    
        const Action = {
    
    
            // 默认方法
            default(i) {
    
    
                return this.gray(i);
            },
            // 红色特效
            red(i) {
    
    
                data[i + 1] = 0;
                data[i + 2] = 0;
                data[i + 3] = opacity;
            },
            // 平均灰度特效
            gray(i) {
    
    
                // 将红、绿、蓝色取平均值
                data[i] = data[i + 1] = parseInt(data[i + 2] = (data[i] + data[i + 1] + data[i + 2]) / 3);
                data[i + 3] = opacity;
            }
        };
        return function (state) {
    
    
            return Action[state] || Action[state]();
        }
    }());

    // 迭代器处理数据
    function eachData(fn) {
    
    
        for (let i = 0; i < data.length; i += 4) {
    
    
            // 处理一组像素数据
            fn(i);
        }
    }
    // 处理数据
    eachData(Deal(effect));

    ctx.putImageData(imageData, x + w, 0);
};

特点

通过迭代器我们可以顺序地访问一个聚合对象中的每一个元素

在开发中,迭代器极大简化了代码中的循环语句,使代码结构清晰紧凑,然而这些简化了的循环语句实质上隐形地移到了迭代器中

迭代器处理一个对象时,我们只需提供处理的方法,而不必去关心对象的内部结构,这也解决对象的使用者与对象内部结构之间的耦合

迭代器的存在也为我们提供了操作对象的一个统一接口

语言翻译-解释器模式

解释器模式(Interpreter)

对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。

需求:描述带有点击事件的DOM元素在页面中的位置

在这里插入图片描述

<!doctype html>
<html lang="zh-cn">

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>语言翻译-解释器模式</title>
</head>
<style>
    * {
      
      
        margin: 15px;
        border: 3px solid #333;
    }

    *:nth-child(2n) {
      
      
        border-color: red;
    }

    *:nth-child(2n + 1) {
      
      
        border-color: orange;
    }
</style>

<body>
    <div>
        <div>
            <div>
                <button onclick="Interpreter(this)">【HTML &gt; HEAD|BODY &gt; DIV &gt; DIV &gt; DIV &gt; BUTTON】</button>
            </div>
            <div>
                <button onclick="Interpreter(this)">【HTML &gt; HEAD|BODY &gt; DIV &gt; DIV &gt; DIV|DIV &gt; BUTTON】</button>
            </div>
        </div>
    </div>
    <ul>
        <li>
            <button onclick="Interpreter(this)">【HTML &gt; HEAD|BODY &gt; DIV|UL &gt; LI &gt; BUTTON】</button>
        </li>
        <li>
            <button onclick="Interpreter(this)">【HTML &gt; HEAD|BODY &gt; DIV|UL &gt; LI|LI &gt; BUTTON】</button>
        </li>
        <li>
            <button onclick="Interpreter(this)">【HTML &gt; HEAD|BODY &gt; DIV|UL &gt; LI|LI|LI &gt; BUTTON】</button>
        </li>
    </ul>
    <div>
        <button onclick="Interpreter(this)">【HTML &gt; HEAD|BODY &gt; DIV|UL|DIV &gt; BUTTON】</button>
        <button onclick="Interpreter(this)">【HTML &gt; HEAD|BODY &gt; DIV|UL|DIV &gt; BUTTON|BUTTON】</button>
        <p onclick="Interpreter(this)" style="text-align: center;">【HTML &gt; HEAD|BODY &gt; DIV|UL|DIV &gt; BUTTON|BUTTON|P】</p>
    </div>
    <script src="./index.js"></script>
</body>

</html>
// XPath 解释器
const Interpreter = (function () {
    
    

    /**
     * 获取兄弟元素的nodeName
     * @param {HTMLElement} node DOM元素
     */
    function getSublingName(node) {
    
    
        let names = [];
        while (node) {
    
    
            if (node.nodeType === 1)
                names = [node.nodeName, ...names];
            node = node.previousSibling;
        }
        console.log(names.join('|'));
        return names.join('|');
    }

    /**
     * 解释器方法
     */
    return function (node) {
    
    
        let path = [];
        while (node) {
    
    
            if (node.nodeType === 1) {
    
    
                path = [getSublingName(node), ...path]
            }
            node = node.parentNode;
        }
        console.log(path.join('>'));
        return path.join('>');
    };
}());

猜你喜欢

转载自blog.csdn.net/weixin_43526371/article/details/127595087