前端之设计模式

268 阅读9分钟

前言

此篇文章会一直持续更新

我们写代码到底是在写什么

设计模式扮演的角色

  1. 帮助我们组织模块
  2. 帮助我们设计沟通
  3. 提高代码质量

设计原则

  1. 开闭原则
  2. 单一职责原则
  3. 依赖倒置原则
  4. 接口隔离原则
  5. 迪米特法则
  6. 里氏替换原则

设会模式分类

  1. 创建型
  2. 结构型
  3. 行为型
  4. 技巧型

创建型

  1. 工厂模式 -- 大量创建对象
  2. 单例模式 -- 全局只能有我一个
  3. 建造者模式 -- 精细化组合对象
  4. 原型模式 -- Javascript的灵魂

封装与对象

封装目的
  1. 定义变量不会污染外部;
  2. 能够作为一个模块调用;
  3. 遵循开闭原则;
什么是好的封装
  1. 变量外部不可见;
  2. 调用接口使用;
  3. 留出扩展接口;
创建对象时的设计模式
  1. 工厂模式
    • 目的:方便我们大量创建对象
    • 应用场景:当某一个对象需要经常创建的时候、如分页对象的获取
    • 基本结构:写一个方法,只需要调用这个方法,就能拿到你要的对象
    function Factory(type) {
        switch (type) {
            case 'type1':
                return new Type1();
            case 'type2':
                return new Type2();
            case 'type3':
                return new Type3();
        }
    }
    
    • 示例:多彩的弹窗(需求:项目有一个弹窗需求,弹窗有多种,他们之间存在内容和颜色上的差异)
    (function() {
        function pop(type, content, color) {
            if (this instanceof pop) {
                var s = new this[type](content, color);
            } else {
                return new pop(type, content, color);
            }
        }
        pop.prototype.infoPop = function(content, color) {
            console.log('消息弹框:' + content + color);
        }
        pop.prototype.cancelPop = function(content, color) {
            console.log('取消消息弹框:' + content + color);
        }
        pop.prototype.confirmPop = function(content, color) {
            console.log('确认消息弹框:' + content + color);
        }
        window.pop = pop;
    })();
    var arr = [{ type: 'infoPop', content: 'hello', color: 'yellow' }, 
    { type: 'cancelPop', content: '取消', color: 'red' }, 
    { type: 'confirmPop', content: '确认', color: 'green' }];
    arr.forEach(item => {
        window.pop(item.type, item.content, item.color);
    })
    
    • 源码示例:jquery (需求:jQuery需要操作dom,每一个dom都是一个jquery对象)
    (function() {
        var jquery = function(selector, contentx) {
            return new jquery.fn.init(selector, contentx);
        }
        jquery.fn = jquery.prototype = {
            init: function() {
    
            }
        }
        jquery.fn.init.prototype = jquery.fn;
        jquery.extend = jquery.fn.extend = function() {
    
        }
        jquery.extend({
    
        })
        window.$ = window.jquery = jquery;
    })();
    
  2. 建造者模式
    • 目的:需要组合出一个全局对象
    • 应用场景:当要创建单个、庞大的组合对象时,如轮播图的实现
    • 基本结构:把一个复杂的类各个部分,拆分成独立的类,然后再在最终类里组合到一块,final为最终给出去的类
    function Module1() {
    }
    
    function Module2() {
    }
    
    function Final() {
        this.module1 = new Module1();
        this.module2 = new Module2();
    }
    
    • 示例:编写一个编辑器插件(需求:有一个编辑器插件,初始化的时候需要配置大量的参数,而且内部功能很多)
    • 源码实例:Vue的初始化(vue内部众多模块,而且过程复杂)
    function Vue(options) {
        if (!(this instanceof Vue)) {
            console.warn('请使用new操作符');
        }
        this._init(options);
    }
    //混入
    initMixin(Vue);
    stateMixin(Vue);
    eventsMixin(Vue);
    lifecycleMixin(Vue);
    renderMixin(Vue);
    
  3. 单例模式
    • 目的:需要确保全局只有一个对象
    • 应用场景:为了避免重复创建,避免多个对象存在互相干扰
    • 基本结构:通过定义一个方法,使用时只允许通过此方法拿到存在内部的同一实例化对象
    let Singleton = function(name) {
        this.name = name;
    }
    Singleton.getInstance = function(name) {
        if (this.instance) {
            return this.instance;
        }
        return this.instance = new Singleton(name);
    }
    
    • 示例:全局的数据存储对象(需求:项目中有一个全局的数据存储者,这个存储者只能有一个,不然会需要进行同步,增加复杂度)
    function store() {
        this.store = {
    
        };
        if (store.install) {
            return store.install;
        }
        store.install = this;
    }
    store.install = null;
    var s1 = new store();
    var s2 = new store();
    s1.store.a = 1;
    console.log(s2)
    
    • 源码实例:vue-router(vue-router必须保障全局有且只有一个,否则的话会错乱)
    let _Vue;
    function install(Vue) {
        if (install.installed && _Vue === Vue) {
            return;
        }
        install.installed = true;
        _Vue = Vue;
    }
    

结构型

  1. 外观模式 -- 给你的一个套餐
  2. 享元模式 -- 共享来减少数量
  3. 适配器模式 -- 用适配代替更改
  4. 桥接模式 -- 独立出来,然后再对接过去
  5. 装饰者模式 -- 更优雅地扩展需求

提高可扩展性

提高可扩展性的目的
  1. 面对需求变更,方便需求更改;
  2. 减少代码修改的难度;
什么是好的可扩展性
  1. 需求的变更,不需要重写;
  2. 代码修改不会引起大规模变动;
  3. 方便加入新模块;
更好的更改代码
  1. 适配器模式
    • 目标:接口
    • 目的:通过写一个适配器,来代替替换
    • 应用场景:面临接口不通用的问题
    • 基本结构
    var log = (function() {
        return window.console.log;
    })();
    
    • 示例:框架的变更(需求:目前项目使用的A框架,现在改成了B,两个框架十分类似,但是有少数几个方法不同)
    A.c();
    //b.css();
    A.o();
    //B.on();
    window.A = B;
    A.c = function() {
        return B.css.apply(this, arguments);
    }
    A.o = function() {
        return B.on.apply(this, arguments);
    }
    
    • 示例:参数适配(需求:为了避免参数不适配产生问题,很多框架会有一个参数适配操作)
    function f1(options) {
        var defaultOptions = {
            name: '',
            age: '',
            sex: '',
            color: 'red'
        };
        for (let key in defaultOptions) {
            defaultOptions[key] = options[key] || defaultOptions[key];
        }
    }
    
  2. 装饰者模式
    • 目标:方法作用
    • 目的:不重写方法的扩展方法
    • 应用场景:当一个方法需要扩展,但是又不好去修改方法
    • 基本结构
    var a = {
        b: function() {
        }
    }
    
    function myb() {
        a.b();
        //TODO 要扩展的方法
    }
    
    • 示例:扩展你的已有事件绑定(需求:现在项目改造,需要给input标签已经有的事件,增加一些操作)
    dom.onclick = function() {
        console.log('1');
    };
    var decorator = function(dom, fn) {
        if (typeof dom.onclick == 'function') {
            var _old = dom.onclick;
            dom.onclick = function() {
                _old();
                fn();
            }
        }
    };
    decorator(document.getElementById('dom1'), function() {
        console.log('还要执行2');
    });
    
    • 示例:Vue的数组监听(需求:vue中利用defineProperty可以监听对象,那数组怎么办)
      var arrayProto = Array.prototype;
      var arrayMethods = Object.create(arrayProto);
    
      var methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
      ];
    
      /**
       * Intercept mutating methods and emit events
       */
      methodsToPatch.forEach(function (method) {
        // cache original method
        var original = arrayProto[method];
        def(arrayMethods, method, function mutator () {
          var args = [], len = arguments.length;
          while ( len-- ) args[ len ] = arguments[ len ];
    
          var result = original.apply(this, args);
          var ob = this.__ob__;
          var inserted;
          switch (method) {
            case 'push':
            case 'unshift':
              inserted = args;
              break
            case 'splice':
              inserted = args.slice(2);
              break
          }
          if (inserted) { ob.observeArray(inserted); }
          // notify change
          ob.dep.notify();
          return result
        });
      });
    
解耦你的方法与调用
  1. 命令模式
    • 目的:解耦实现和调用,让双方互不干扰;
    • 应用场景:调用的命令充满不确定性
    • 基本结构
    var command = (function() {
        var action = {};
        return function excute() {}
    })();
    
    • 示例:绘图命令(需求:封装一系列的canvas绘图命令)
    var canvasCommand = (function() {
         var actions = {
             drawCirecle: function() {
     
             },
             drawRect: function() {
     
             }
         };
         return function execute(commander) {
             if (Array.isArray(commander)) {
                 commander.forEach(c => {
                     let command = c.command;
                     actions[command](c.config);
                 });
             }
         }
     })();
     let commander = [
         { command: 'drawCirecle', config: {} },
         { command: 'drawCirecle', config: {} },
         { command: 'drawRect', config: {} }
     ];
     canvasCommand(commander);
    
    • 示例:绘制随机数量图片(需求:要做一个画廊,图片数量和排列顺序随机)
    var createImg = (function() {
         var actions = {
             create: function(options) {
                 var htmlArr = [];
                 var _htmlstring = '';
                 var _htmlTemplate = "<div><img src='{{img-url}}'/></div><h2>{{title}}</h2>";
                 //策略模式
                 var displayWay = {
                     normal: function(arr) {
                         return [];
                     },
                     reverse: function(arr) {
                         return [];
                     }
     
                 };
                 options.imgArr.forEach(img => {
                     var _html;
                     _html = _htmlTemplate.replace('{{img-url}}', img.img).replace('{{title}}', img.title);
                     htmlArr.push(_html);
                 });
                 htmlArr = displayWay[options.type](htmlArr);
                 _htmlstring = htmlArr.join('');
                 return "<div>" + _htmlstring + "</div>";
             },
             display: function(options) {
                 let _html = this.create(options);
                 options.target.innerHTML = _html;
         =764
         };
         return function execute(obj) {
             var _default = {
                 imgArr: [{ img: 'xxx', title: 'img1' }],
                 type: 'normal',
                 target: document.body
             };
             //适配器模式,适配参数
             for (var key in _default) {
                 _default[key] = obj[key] || _default[key];
             }
             actions.display(_default);
         }
     })();
     createImg({});
    

行为型

  1. 观察者模式 -- 我作为第三方转发
  2. 职责链模式 -- 像生产线一样组织模块
  3. 状态模式 -- 用状态代替判断
  4. 命令模式 -- 用命令去解耦
  5. 策略模式 -- 算法工厂
  6. 迭代器模式 -- 告别for循环

优化你的代码结构

  1. 策略模式/状态模式
    • 目的:优化if-else分支
    • 应用场景:当代码if-else分支过多时
    • 基本结构(策略模式)
    function Strategy(type, a, b) {
        var strategyer = {
            add: function(a, b) {
                return a + b;
            },
            minus: function(a, b) {
                return a - b;
            },
            division: function(a, b) {
                return a / b;
            }
        }
        return strategyer[type](a, b);
    }
    
    假设要编写一个计算器,有加减乘除,我们可以把一层一层的if判断,变成上面的形式
    • 基本结构(状态模式)为了减少if-else结构,将状态变成对象内部的一个状态,通过对象内部的状态改变,让其拥有不同行为
    function stateFacotr(status) {
        var stateObject = {
            _status: '',
            state: {
                state1: function() {
    
                },
                state2: function() {
    
                }
            },
            run: function() {
                return this.state[this._status];
            }
        }
        stateObject._status = status;
        return stateObject;
    }
    stateFacotr('state1').run();
    
    • 示例:(需求:项目有一个动态的内容,根据用户权限的不同显示不同的内容)
    function showControll() {
        this.permission = '';
        this.power = {
            boss: function() {
                console.log('boss权限')
            },
            manager: function() {
                console.log('经理权限')
            },
            staff: function() {
                console.log('普通员工权限')
            }
        }
    }
    showControll.prototype.show = function() {
        var _self = this;
        axios.get('xxx').then((res) => {
            _self.permission = res;
            _self.power[this.permission]();
        });
    }
    
    new showControll().show();
    
    • 示例:复合运动(需求:有一个小球,可以控制它左右移动,或者左前,右前等方式移动)
        function mover() {
            this.satus = [];
            this.actionHandle = {
                left: moveLeft,
                right: moveRight,
                top: moveTop,
                bottom: moveBottom
            }
        }
        mover.prototype.run = function() {
            this.status = Array.prototype.slice.call(arguments);
            this.status.forEach(action => {
                this.actionHandle[action]();
            })
        }
        new mover().run('left', 'top');
    
  2. 外观模式
    • 目的:通过为多个复杂的子系统提供一个一致的接口
    • 应用场景:当完成一个操作,需要操作多个子系统,不如提供一个更高级的统一接口
    • 基本结构:我们在组织方法模块时可以细化多个接口,但是我们给别人使用时,要合为一个接口,就像你可以直接去餐厅点套餐
    function Model1() {
    }
    
    function Model2() {
    }
    //功能由Model1获取Model2的结果来完成
    function use() {
        Model2(Model1());
    }
    
    • 示例:插件封装的规律(需求:插件基本上都会给最终使用提供一个高级接口)
    • 示例:封装成方法的思想(需求:在兼容时代,我们会常常需要检测能力,不妨作为一个统一接口)
    function addEvent(dom, type, fn) {
        if (dom.addEventListener) {
            dom.addEventListener(type, fn, false);
        } else if (dom.attachEvent) {
            dom.attachEvent('on' + type, fn);
        } else {
            dom['on' + type] = fn;
        }
    }
    
  3. 迭代器模式
    • 目的:不访问内部的情况下,方便的遍历数据
    • 应用场景:当我们要对某个对象进行操作,但是又不能暴露内部
    • 基本结构:在不暴露对象内部结构的同时,可以顺序的访问对象内部的,可以帮助我们简化循环,简化数据操作
    function Iterator(item) {
        this.item = item;
    }
    Iterator.prototype.dealEach = function(fn) {
        for (var i = 0; i < this.item.length; i++) {
            fn(this.item[i], i);
        }
    }
    
    • 示例:典型的forEach就是迭代器(构建一个自己的forEach,循环数组和对象)
    function Iterator(data) {
        this.data = data;
    }
    Iterator.prototype.dealEach = function(fn) {
        if (this.data instanceof Array) {
            for (let i = 0; i < this.data.length; i++) {
                fn(this.data[i], i);
            }
        } else {
            for (let key in this.data) {
                fn(this.data[key], key);
            }
        }
    }
    
    • 示例:给你的项目数据添加迭代器(需求:项目会经常对于后端数据进行遍历操作,不如封装一个迭代器,遍历的更方便)
        var data = [
        { num: 1 },
        { num: 2 },
        { num: 3 }
    ];
    
    function iteratorFactory(data) {
        function Iterator(data) {
            this.data = data;
        }
        Iterator.prototype.geHasSomenum = function(handler, num) {
            var _arr = [];
            var handleFn;
            if (typeof handler == 'function') {
                handleFn = handler;
            } else {
                handleFn = function(item) {
                    if (item[handler] === num) {
                        return item;
                    }
                }
            }
            for (let i = 0; i < this.data.length; i++) {
                var _result = handlerFn.call(this, this.data[i]);
                if (_result) {
                    _arr.push(_result);
                }
            }
            return _arr;
        }
    }
    iteratorFactory(data).geHasSomenum('num', 1);
    iteratorFactory(data).geHasSomenum(function(item) {
        if (item.num - 1 == 2) {
            return item;
        }
    });
    
  4. 备忘录模式
    • 目的:记录状态,方便回滚
    • 应用场景:系统状态多样,为了保证状态的回滚方便,记录状态
    • 基本结构:记录对象内部的状态,当有需要时回滚到之前的状态或者方便对象使用
    function Memento() {
        var cache = {};
        return function(cacheName) {
            if (cache[cacheName]) {
                //有缓存的操作的
            } else {
                //没缓存的操作
            }
        }
    }
    var MementoFn = Memento();
    MementoFn('xxx');
    
    • 示例:文章页缓存(需求:项目有一个文章页需求,现在进行优化,如果上一篇已经读取过了,则不进行请求,否则请求文章数据)
    function pager() {
        var cache = {};
        return function(pageName) {
            if (cache[pageName]) {
                return cache[pageName];
            } else {
                axios.get(options).then(res => {
                    cache[pageName] = res;
                })
            }
        }
    }
    
    • 示例:前进后退功能(需求:开发一个可移动的div,拥有前进后退功能回滚到之前的位置)
    function moveDiv() {
        this.sateList = [];
        this.nowState = 0;
    }
    moveDiv.prototype.move = function(type, num) {
        changeDiv(type, num);
        this.sateList.push({
            type: type,
            num: num
        });
        this.nowState = this.sateList.length - 1;
    }
    move.prototype.go = function() {
        var _state;
        if (this.nowSate < this.sateList.length - 1) {
            this.nowState++;
            _state = this.sateList[this.nowState];
            changeDiv(_state.type, _state.num);
        }
    }
    

技巧型

  1. 链模式 -- 链式调用
  2. 惰性模式 -- 我就要搞机器学习
  3. 委托模式 -- 让别人代替你收快递
  4. 等待者模式 -- 等你们都回来再吃饭
  5. 数据访问模式 -- 一个方便的数据管理器