详解javascript常用的设计模式

210 阅读6分钟

前言

在日常编写JS代码的过程中,运用一定的设计模式可以让我们的代码更加优雅,灵活。

工厂模式

工厂就是大批量的产生相似产品的地方。那简单的工厂模式就可以理解为解决多个相似的问题。

    // 工厂函数
    function Person(name, age) {
        let obj = new Object();
        obj.name = name;
        obj.age = age;
        obj.say = function () {
            console.log(this.name);
        };
        return obj;
    }
    person1 = new Person('Joke', 25);
    person2 = new Person('Nike',18);
    person1.say();         // Joke
    person2.say();         // Nike

优点

能解决多个相似问题

缺点

工厂函数太僵化,创建实例不能对其扩展,无法实现实例对象的个性化

优化

强类型语言里面有个名词抽象类,抽象类就是这个类只能被继承,不能实例化,我们现在就把这个工厂函数包装成一个抽象类,然后这个类实现基本属性的初始化,其他个性的东西就等子类继承它的时候另外扩展。

    // 父类(抽象类)
    function BicycleShop(name) {
        this.name = name;
    }
    BicycleShop.prototype = {
        constructor: this,
        showName() {
            return this.name;
        },
        sellBicycle() {
            let bicycle = this.createBicycle();
            bicycle.A();
            bicycle.B();
            return bicycle;
        },
        createBicycle() {
            throw new Error("父类是抽象类不能直接调用,需要子类重写方法");
        }
    };
    // 继承函数
    function extend(Sub,Sup) {
        let F = new Function();
        F.prototype = Sup.prototype;
        Sub.prototype = new F();
        Sub.prototype.constructor = Sub;
    }
    // 子类
    function BicycleChild(name) {
        // 初始父类属性
        BicycleShop.call(this,name);
    }
    // 继承父类
    extend(BicycleChild,BicycleShop);
    // 实例化一个子类对象,测试是否可以直接使用createBicycle方法
    let child1 = new BicycleChild('fenghuang');
    child1.sellBicycle();   // 抛出异常
    // 重写父类的createBicycle方法
    BicycleChild.prototype.createBicycle = function() {
        let A = function () {
            console.log("执行业务A");
        };
        let B = function () {
            console.log("执行业务B");
        };
        return {
            A: A,
            B: B
        }
    };
    // 再实例化一个子类对象
    let child2 = new BicycleChild('fenghuang');
    child2.sellBicycle();    // 执行A,B函数

单体模式

单体模式的特点就是:只能被实例化一次
例如,创建弹窗,如果弹窗不存在,新建;弹窗存在,直接返回存在的弹窗

    function CreateDialog(html) {
        this.html = html;
        this.init();
    }
    CreateDialog.prototype.init = function () {
        let div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
    };
    let Dialog = (function () {
        // 闭包实现变量缓存
        let instance;
        return function (html) {
            // 如果instance为null,则实例化CreateDialog,否则,直接返回
            if(!instance) {
                instance = new CreateDialog(html);
            }
            return instance;
        }
    })();
    let a = Dialog("aaa");
    let b = Dialog("bbb");
    console.log(a===b);   // true

优点

只能被实体化一次,特别适合新增dom元素的操作

代理模式

例如,异地恋的情侣,男生想给女生送杯奶茶喝,但是他自己无法做到,这时候就得求助美团,让美团代自己把奶茶送给自己的女朋友,这就是代理模式。

    function Girlfriend(name) {
        this.name = name;
    }
    function Boyfriend(girl) {
        this.girl = girl;
        this.showLove = function (gift) {
            console.log("Hi" + this.girl.name + ", 你的男朋友送了你" + gift);
        }
    }
    function Meituan(girl) {
        this.girl = girl;
        this.send = function (gift) {
            (new Boyfriend(this.girl)).showLove(gift);
        }
    }
    let mt = new Meituan(new Girlfriend("小雪"));
    mt.send("奶茶");    // Hi小雪, 你的男朋友送了你奶茶

优点

男生只需要关注结果,中间买奶茶,送奶茶的过程不需要关心

职责链模式

例如,工地上有老板,有白、红、黄帽子,老板派一个任务,如果白帽子执行不了就把任务传递给红帽子,红帽子执行不了就传递给黄帽子,这样就形成了一个职责链条。

    function bigBoss(task) {
        console.log("谁来"+ task);
        return "nextExecutor";
    }
    function whiteHat(task) {
        if(task === "泡澡") {
            console.log("泡澡这种事就来找我");
        }else {
            console.log("我戴的是白帽子,我只喜欢泡澡!");
            return "nextExecutor";
        }
    }
    function redHat(task) {
        if(task === "喝酒") {
            console.log("喝酒这种事就来找我");
        }else {
            console.log("我戴的是红帽子,我只喜欢喝酒!");
            return "nextExecutor";
        }
    }
    function yellowHat(task) {
        console.log("我来"+task);
    }

    function Build(fn) {
        this.fn = fn;
        this.executor = null;
    }
    // 设置下一级
    Build.prototype.setNextExecutor = function (executor) {
        return this.executor = executor;
    };
    // 给下一级派发任务
    Build.prototype.dispatchTask = function() {
        let ret = this.fn.apply(this, arguments);
        if(ret === "nextExecutor") {
            return this.executor && this.executor.dispatchTask.apply(this.executor, arguments);
        }
        return ret;
    };
    let boss = new Build(bigBoss);
    let white = new Build(whiteHat);
    let red = new Build(redHat);
    let yellow = new Build(yellowHat);

    boss.setNextExecutor(white);
    white.setNextExecutor(red);
    red.setNextExecutor(yellow);

    boss.dispatchTask("泡澡");
    boss.dispatchTask("喝酒");
    boss.dispatchTask("盖房子");

优点

  1. 解耦,发送者只需要把请求发给第一个接收者即可
  2. 职责链中增加一个接收者或者删除一个接收者都比较容易

命令模式

这个模式其实在开发中经常用到,比如我们给一个按钮添加了一个click事件,点我们触发这个click事件的时候,其实就是执行了命令。

    let btn = document.querySelector('#btn');
    btn.addEventListener('click',function (e) {
       console.log("按钮事件被触发");
    },false);
    btn.click();

依据上面的代码,我们就可以生成如下的命令模式

    // 创建命令内容1,2,3
    let commmand1 = {
        content: function() {
            console.log('命令1');
        }
    };
    let commmand2 = {
        content: function() {
            console.log('命令2');
        }
    };
    let commmand3 = {
        content:function() {
            console.log('命令3');
        }
    };
    // 宏命令,包含命令的添加方法和执行方法
    let command = function () {
        return {
            commandList: [],
            add(command) {
                this.commandList.push(command);
            },
            excute() {
                for(let item of this.commandList) {
                    item.content();
                }
            }
        }
    };
    // 发布命令的对象
    let c = command();
    // 添加命令
    c.add(commmand1);
    c.add(commmand2);
    c.add(commmand3);
    // 执行命令
    c.excute();

优点

一个指令可以执行多个命令

策略模式

《孙子兵法》讲的就是策略,根据不同的形势制定相应的运动方针,我们就用代码来实现孙子兵法

    function sunzi(situation) {
        if (situation === 'A') {
            console.log("A形势,执行三十六计之瞒天过海")
        }
        if (situation === 'B') {
            console.log("B形势,执行三十六计之围魏救赵")
        }
        if (situation === 'C') {
            console.log("C形势,执行三十六计之借刀杀人")
        }
        // ...剩余33计
    }
    sunzi('A');

这就是策略模式,但是代码中有大量的if判断,非常损耗性能,所以需要优化一下

    let optimizeSunzi = {
        A() {
            console.log("A形势,执行三十六计之瞒天过海");
        },
        B() {
            console.log("B形势,执行三十六计之围魏救赵")
        },
        C() {
            console.log("C形势,执行三十六计之借刀杀人")
        }
    };
    let getHelp = function (situation) {
        return optimizeSunzi[situation]();
    };
    getHelp('B');

优点

代码更容易理解,也更容易扩展,同时代码也可以复用

发布订阅模式

京东上有一款鞋子处于缺货状态,小红和小绿都看中了它,但是她俩不知道到底什么时候才有货,不然得无时无刻的去京东上看,此时京东有一个功能到货通知,如果小红和小绿都启用了这个功能,一旦这个鞋子有货了就会发信息给她俩,这就是发布订阅模式。京东是发布者,小红和小绿是订阅者

    let publish = (function () {
        // 订阅者容器
        let subArr = [];
        return {
            add(fn) {
                subArr.push(fn);
            },
            trigger() {
                for (let item of subArr) {
                    item.apply(this, arguments);
                }
            }
        }
    })();
    // 添加两个订阅者
    publish.add(function (color,size) {
        console.log("订阅者1:颜色("+color+")尺码("+size+")");
    });
    publish.add(function (color,size) {
        console.log("订阅者2:颜色("+color+")尺码("+size+")");
    });
    // 发布消息
    publish.trigger("红色",42);

优点

  1. 支持简单的广播通信,一旦对象状态改变,就会自动通知订阅的对象
  2. 发布者和订阅者耦合性降低

缺点

创建订阅者需要消耗时间和内存,切勿大量使用。

中介者模式

中介,即连接两方的一个环节。比如:裁判
用跑男举例,比赛两方红和蓝,比赛开始前分队伍,确认队友与敌人,比赛开始后红蓝双方开始撕名牌,名牌被撕掉裁判就通知"xx被淘汰",如果某一方的所有队员的名牌都被撕掉,裁判宣判比赛结束。

    function Referee(name, team) {
        this.name = name;     // 姓名
        this.team = team;     // 队伍
        this.state = 'live';  // 当前状态
        this.friends = [];    // 队友
        this.enemies = [];    // 敌人
    }

    Referee.prototype.lose = function (allDead) {
        // 如果一方全体淘汰,则该队伍失败
        if (allDead) {
            console.log(this.team + "团队失败!");
            console.log(this.enemies[0].team + "团队胜出!");
        } else {
            console.log(this.team + "方" + this.name + "淘汰!");
        }
    };
    Referee.prototype.die = function () {
        this.state = 'die';
        let all_dead = true;
        for (let item of this.friends) {
            if (item.state === 'live') {
                all_dead = false;
                break;
            }
        }
        this.lose();
        if (all_dead) {
            this.lose(true);
        }
    };
    let signUp = (function () {
        // 队员列表
        let playerList = [];
        return function (name, team) {
            let newPlayer = new Referee(name, team);
            playerList.push(newPlayer);
            // 设置每个队员的队友与敌人
            for (let item of playerList) {
                if (item !== newPlayer) {
                    if (item.team === newPlayer.team) {
                        newPlayer.friends.push(item);
                        item.friends.push(newPlayer);
                    } else {
                        newPlayer.enemies.push(item);
                        item.enemies.push(newPlayer);
                    }
                }
            }
            return newPlayer;
        }
    })();
    // 队员注册
    let p1 = signUp('aa', "red");
    let p2 = signUp('bb', "red");
    let p3 = signUp('cc', "red");
    let p4 = signUp('dd', "red");
    let p5 = signUp('ee', "blue");
    let p6 = signUp('ee', "blue");
    let p7 = signUp('ee', "blue");
    let p8 = signUp('ee', "blue");
    // 淘汰
    p1.die();
    p2.die();
    p3.die();
    p4.die();

优点

依旧是代码更容易理解,耦合性低

结束

其实JS模式在日常代码中经常性用到,或许只是你没有细心注意。