JS中的设计模式

JS中的设计模式

Posted by SkioFox on July 5, 2019
  1. 单例模式

定义:保证一个类只有一个实例,并提供一个访问他的全局访问点。

简介:单例模式是一种常用的模式,我们在多次引入其他模块时,并不需要每次都创建一个新的模块对象,复用之前创建过的对象不仅能减少内存的开销,同时也可以体验共享对象带来的便利。简单来说就是使用闭包持久保存函数上一次的执行结果,在之后的调用中直接返回。

使用场景:比如自定义弹窗,无论你程序中多少调用,都只应创建一个弹窗对象.例如js 中模块加载的方式:require、import都使用到了该模式

    // ES5 实现
    var getSingle = function (fn) { // 创建单例方法
        var result // 通过闭包保存创建过的对象
        return function () {
            // console.log(this)
            return result || (result = fn.apply(this, arguments))
        }
    }

    var createPerson = getSingle(function (name) {
        return {name: name}
    })

    var person1 = createPerson('张三')
    var person2 = createPerson('李四')

    console.log(person1, person2);  // {name: '张三'} {name: '张三'}
    // ES6
    class CreateUser {
        constructor(name) {
            this.name = name;
            this.getName();
        }
        getName() {
            return this.name;
        }
    };

    const ProxyMode = (() => {
        let instance = null;
        return (name) => {
            if(!instance) {
                instance = new CreateUser(name);
            }
            return instance;
        }
    })();

    let a = ProxyMode('vn');
    let b = ProxyMode('lb');

    console.log(a, b);   // vn  vn    单例模式只会创建一次实例
  1. 策略模式

定义:定义一系列算法,把他们一个个封装起来,并且可以相互替换

一个基于策略模式的程序至少由两部分组成:

第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。

第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明Context 中要维持对某个策略对象的引用

简介:现实生活中当我们要达成一个目的的时候通常会有多种方案可以选择。比如马上年底要发年终奖了,公司针对不同类型的员工有不同的奖金策略,对于表现突出的员工发3个月的工资,一般的员工发2个月的,打酱油的发1个月的,专写Bug的扣一个月。在这里 发年终奖 是目的,针对表现不同的员工我们有多种发奖金的策略,可以使用策略模式来实现。

应用场景:表单验证等。

    // ES5
    var strategies = { // 针对不同表现的员工定制策略,每个策略接受同类型的参数返回相同的结果
        S(salary) {
            return salary * 3
        },
        A(salary) {
            return salary * 2
        },
        B(salary) {
            return salary
        },
        C(salary) {
            return -salary
        }
    }

    var calculateBonus = function (salary, strategy) {
        return strategies[strategy](salary)
    }

    console.log(calculateBonus(10000, 'S')); // 30000
    console.log(calculateBonus(1000, 'C')); // -1000 

    // ES6
    const levelObj = {
        "A": money => money * 4,
        "B": money => money * 3,
        "C": money => money * 2
    }

    // 环境类  封装调用接口
    const getMoney = (level, money) => levelObj[level](money);

    console.log(getMoney('A', 200))   // 800
  1. 代理模式

定义:当直接访问一个对象不方便或者不满足需要时,为其提供一个替身对象来控制对这个对象的访问

简介:代理模式是一种非常有意义的模式,在我们日常开发中有许多常用功能都可以通过代理模式实现的,例如防抖动函数(debounce常用于控制用户输入后回调函数触发的时机),节流函数(throttle常用于控制resize、scroll等事件的触发频率)

应用场景:图片懒加载,先缓存动态 loading,必要时传入 src

    // ES5 节流函数
    var throttle = function (fn, interval) {
        var firstTime, timer
        return function () {
            var _this = this
            if(!firstTime) {
                fn.apply(this, arguments)
                firstTime = true
            }
            
            if (!timer) {
                timer = setTimeout(function() {
                    fn.apply(_this, arguments)
                    timer = null
                }, interval);
            }
        }
    }

    var onScroll = function () {
        console.log('onScroll', Date.now())
    }
    var throttleOnScroll = throttle(onScroll, 2000)
    // ES6
    const imgFunc = (() => {
        let imgNode = document.createElement('img');
        document.body.appendChild(imgNode);
        return {
            setSrc: (src) => {
                imgNode.src = src;
            }
        }
    })();

    const ProxyImg = (() => {
        let img = new Image();
        img.onload = () => {
            let node = document.getElementsByTagName('img')
            imgFunc.setSrc(img.src);
        }
        return {
            setSrc: (src) => {
                imgFunc.setSrc('../C3photo/jacky/1.jpg');
                img.src = src;
            }
        }
    })();
    ProxyImg.setSrc('../C3photo/jacky/2.jpg');
  1. 迭代器模式

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

简介:迭代器模式简单来说就是将迭代过程从业务逻辑中抽离,简化开发,其分为内迭代和外迭代。目前许多语言都已经内置了迭代器的实现,如ES5中的forEach函数就是一种内迭代的实现。

    // ES5
    Array.prototype.myEach = function (cb) {
        for (let index = 0; index < this.length; index++) {
            const element = this[index];
            if(cb(element, index) === false) {
                break
            }
            
        }
    };

    ['a','b','c'].myEach(console.log) // a b c
  1. 发布订阅模式

定义:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

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

应用场景:发布订阅模式在我们日常开发中应用十分广泛,如浏览器的dom事件通知机制(document.addEventListener),以及vue框架中数据改变时自动刷新dom的双向绑定机制都是基于该模式,微信公众号订阅。

    // ES5 事件发布与订阅
    var Event = function () {
        var clientList = {} // 订阅者数组

        this.listen = function (key, cb) { // 订阅方法
            clientList[key] = clientList[key] || []
            clientList[key].push(cb)
        }

        this.remove = function (key, cb) { // 取消订阅
            var fns = clientList[key]
            if(!cb) {
                clientList[key] = []
            }else if(fns && fns.length) {
                clientList[key] = fns.filter(fn => fn !== cb)
            }
        }

        this.trigger = function () { // 通知订阅者
            var key = Array.prototype.shift.call(arguments)
            var args = arguments
            var fns = clientList[key]
            var _this = this

            if(fns && fns.length) {
                fns.myEach(function(fn) {
                    fn.apply(_this, args)
                })
            }
        }
    }

    var event = new Event()

    event.listen('phone', function getPhone() {
        Array.prototype.unshift.call(arguments, '有个挨千刀的半夜打电话来了他是:')
        console.log.apply(this, arguments)
    })

    event.trigger('phone', '大狗子') // 有个挨千刀的半夜打电话来了他是:大狗子
    event.trigger('phone', '二狗子') // 有个挨千刀的半夜打电话来了他是:二狗子
    // ES6 微信公众号订阅
    let eventEmitter = {
        list: {},                        // 缓存列表(调度中心)

        on(event, fn) {              // 订阅
            let _this = this;
            _this.list[event] = _this.list[event] || [];    
            _this.list[event].push(fn);
            return _this;
        },

        emit() {               // 发布
            let _this = this;
            let event = [].shift.call(arguments),     // shift 会改变原数组,因此 arguments 只剩下第二个参数
            fns = _this.list[event];
            if(fns && fns.length) {
                fns.forEach(fn => fn.apply(_this, arguments));
            }
            return _this;
        },

        off(event, fn) {                                 // 取消订阅
            let _this = this;
            let fns = _this.list[event];
            if(!fns) return false;          // 如果缓存列表中没有相应的 fn,返回false
            if(!fn) {
                // 如果没有传 fn 的话,就会将 event 值对应缓存列表中的 fn 都清空
                fns.length = 0;
            } else {
                // 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可
                for (let i = 0; i < fns.length; i++) {
                    if (fns[i] === fn || fns[i].fn === fn) {
                        fns.splice(i, 1);
                        break;
                    }
                }
            }
        }
    };

    const user1 = (content) => {
        console.log('用户1订阅了:', content);
    }

    const user2 = (content) => {
        console.log('用户2订阅了:', content);
    }

    const user3 = (content) => {
        console.log('用户3订阅了:', content);
    }

    // 订阅
    eventEmitter.on('article1', user1);
    eventEmitter.on('article1', user2);
    eventEmitter.on('article2', user3);

    eventEmitter.emit('article1', 'Javascript 发布-订阅模式');
    eventEmitter.emit('article2', 'Javascript 观察者模式');

    eventEmitter.off('article1', user1);
    eventEmitter.emit('article1', 'Javascript 发布-订阅模式');


    //用户1订阅了: Javascript 发布-订阅模式
    //用户2订阅了: Javascript 发布-订阅模式
    //用户3订阅了: Javascript 观察者模式
    //用户2订阅了: Javascript 发布-订阅模式
  1. 命令模式

定义:将一组行为抽象为对像并提供执行、撤销等方法,解决它与调用者的之间的耦合关系

简介:命令模式是对简单优雅的模式之一,其中“命令”指的是一个执行某些特定事情的指令。该模式适用于需要向某些对象发出请求,但不知道接受者是谁,也不知道要执行哪些操作。例如我们平时去饭店点菜是我们并不需要知道这道菜是谁做的怎么做的,我们只需要请服务员把需求写在订单上就可以了。

    var client = { // 顾客(命令发出者)
        name: '铁蛋儿'
    }
    var cook = { // 厨师(命令发执行者)
        makeFood: function (food) {
            console.log('开始做:', food)
        },
        serveFood: function (client) {
            console.log('上菜给:', client.name)
        }    
    }

    function OrderCommand(receiver, food) { // 命令对象
        this.receiver = receiver
        this.food = food
    }

    OrderCommand.prototype.execute = function (cook) { // 提供执行方法
        cook.makeFood(this.food)
        cook.serveFood(this.receiver)
    }

    var command = new OrderCommand(client, '宫保鸡丁')
    command.execute(cook) // 开始做:宫保鸡丁; 上菜给铁蛋儿
  1. 组合模式

定义:将一系列具有相同方法的对象合并成一个具有该方法的组合对象,统一执行

简介:组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。同时利用对象的多态性,使得单个对象的使用和组合对象的使用具有一致性。例如我们通过命令模式定义了一系列的命令,并且希望组合这些命令形成一个命令宏统一的执行。

    // 定义一些命令
    var openDoorCommand = {
        execute: function(){
            console.log('开门')
        }
    }

    var openPcCommand = {
        execute: function(){
            console.log('开电脑')
        }
    }

    var openLolCommand = {
        execute: function(){
            console.log('撸一局')
        }
    }

    // 定义命令宏组合命令
    var MarcoCommand = {
        list: [],
        add: function (command) {
            this.list.push(command)
        },
        execute: function () {
            this.list.forEach(function(command) {
                command.execute()
            })
        }
    }

    MarcoCommand.add(openDoorCommand)
    MarcoCommand.add(openPcCommand)
    MarcoCommand.add(openLolCommand)
    MarcoCommand.execute() // 开门 开电脑 撸一局
  1. 装饰者模式:

定义:装饰者模式能够在不改变对象自身的基础上,在运行程序期间给对象动态地添加职责。

使用场景:类似于拦截器,添加对象的前置和后置事件等,react中的高阶组件。

    Function.prototype.before = function(beforefn) {
        let _self = this;                          //保存原函数引用
        return function(){                         //返回包含了原函数和新函数的 '代理函数'
            beforefn.apply(this, arguments);       //执行新函数,修正this
            return _self.apply(this, arguments);   //执行原函数
        }
    }
    Function.prototype.after = function(afterfn) {
        let _self = this;
        return function(){
            let ret = _self.apply(this, arguments);
            afterfn.apply(this, arguments);
            return ret;
        }
    }
    let func = function() {
        console.log('2');
    }
    //func1和func3为挂载函数
    let func1 = function() {
        console.log('1');
    }
    let func3 = function() {
        console.log('3');
    }

    func = func.before(func1).after(func3);
    func();   // 1  2  3