让你秒懂四种设计模式!

3,140 阅读12分钟

0. 回顾过去

说实话,这标题有点儿 uc小编 的味道了。虽然真正的大佬已经对设计模式烂熟于胸,只希望我的学习记录能帮助到部分同学就足够了。经过我下面的介绍,你可以在极短的时间,了解并知道如何使用他们。

经过一两个月的分享断断续续的,我一共分享了 11 种 Javascript 设计模式,其中:

每一种设计模式都是前辈们总结多年的经验,实属精华。尽管我不能完全出神入化的运用它,但在一个程序但设计上,一定会或多或少的来借鉴他们的思想。自从学习了设计模式,在写代码的时候,终于不会气喘吁吁,一口气写五个组件,不费劲。

那下面,接着介绍四种,令人拍案叫绝的!设计模式。

1. 原型模式

简介

原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。

说实话,每次一看设计模式的简介就头大,明明每个字都认识,结合起来就懵了。和我一样情况的小伙伴,建议直接看代码,有的时候...看看注释,看看设计模式的名字,也许就豁然开朗!

实际操作

原型模式的使用可以让我们得到,原始对象附带的一些属性,如下:

var lol = {
    server: '比尔吉沃特',
    startGame: function () {
        console.log('link start!')
    }
};

// 通过原型模式,新建同一个服务器的新用户
var User = Object.create(lol, {
    'name': {
        value: '黄梵高'
    }
});

上面可以说就是一个完整的原型模式。把原有对象内包含的属性,通过Object.create函数,成功拷贝。并且在创建新对象时,添加进入新增的name字段,十分人性化。

如果说你觉得,哦,我这要兼容 ie8 的,用不来Object.create这种高级浏览器才支持的函数。那完全可以的,下面再来介绍不使用这函数的原型模式实现:

var lol = {
    server: '比尔吉沃特',
    startGame: function () {
        console.log('link start!')
    },
    init: function (name) { // 需要增加一个接口,用于修改内部属性
        this.name = name
    },
};


function User(name) {
    function F() { };
    F.prototype = lol;
    
    var f = new F();
    f.init(name);
    return f;
}

var user = User('黄梵高');
user.startGame();
// user.server

这种原型模式的实现方式,和上面的Object.create方式略有不同,因为创建了一个名叫F的构造函数,并且提前暴漏了接口init才得以修改内部属性,和直接创建对象相比自然是冗余了不少。

不过相信兼容 ie8 的需求也还是存在的,很多时候还是不得不使用它啊!

那恭喜你学会了原型模式,因为十分简单,平时开发也会不经意间用到,但要注意浅拷贝和深拷贝的问题哦。先来道开胃菜,这波啊这波是一道肉蛋葱鸡。

2. 观察者模式

介绍

观察者模式定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

实践环节

其实观察者模式,真的太熟悉了。就是当你开源项目终于发布了,你希望很多人都知道,但不能你一个一个去告诉吧,那也太卑微了。所以最好的方式是你有一群粉丝,他们翘首以盼着等着你的开源项目,终于你在b站,开了一个发布会。很多粉丝争先恐后地去听,听完之后回家根据这个新框架开始练手。这就构成了一个观察者模式。

观察者的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。

先来一个错误的❌例子:


var timer = setInterval(() => {
    if (window.good) {
        clearInterval(timer)
        console.log('good了')
    }
}, 500)

setTimeout(() => {
    window.good = 1
}, 2000)

  • s: 实现成功!教练我学会了!这边疯狂修改全局变量,另一边疯狂event loop就好了,500毫秒延迟太高了,我再低一点,调个50吧,再来一个 for 循环,遍历创建事件就可以了!

  • t: 且不说性能问题,那如果页面很多,你在某一个页面抛出的变量一定能接的到吗?或者会不会有变量冲突,多人维护的时候怎么能保证全局变量的统一处理?

  • s: 那教练我不会了。

  • t: 下面教练教你一招,通过存放回调函数的方式队列执行:

var pubsub = {};
(function (q) {
    var topics = {}, // 回调函数存放的数组,把所有的
        subUid = -1;
    // 发布方法
    q.publish = function (topic, args) {
        if (!topics[topic]) {
            return false;
        }
        setTimeout(function () {
            var subscribers = topics[topic], // 名字为topic变量的事件队列
                len = subscribers ? subscribers.length : 0;
            while (len--) {
                subscribers[len].func(topic, args); // 循环执行传进来的函数
            }
        }, 0); // 使用setTimeout保证先执行完同步代码逻辑
        return true;
    };
    //订阅方法
    q.subscribe = function (topic, func) {
        if (!topics[topic]) {
            topics[topic] = [];
        }
        var token = (++subUid).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };
    //退订方法
    q.unsubscribe = function (token) {
        for (var m in topics) { // 先找到要退订的事件队列
            if (topics[m]) {
                for (var i = 0, j = topics[m].length; i < j; i++) { // 传进来的token,是在触发订阅函数时生成的token
                    if (topics[m][i].token === token) { // 找到专属token,进行队列删除操作
                        topics[m].splice(i, 1);
                        return token;
                    }
                }
            }
        }
        return false;
    };
} (pubsub));

//将订阅赋值给一个变量,以便退订
var sub = pubsub.subscribe('lol', function (topics, data) {
    console.log(topics + ": " + data);
});

//发布通知
pubsub.publish('lol', 'hello world!');
pubsub.publish('lol', ['test', 'a', 'b', 'c']);
pubsub.publish('lol', [{ 'color': 'blue' }, { 'text': 'hello'}]);

setTimeout(function () {
    pubsub.unsubscribe(sub);
}, 0);

pubsub.publish('lol', 'hello world!'); // 不会再执行订阅时传入的函数了

以上就实现了一个,以事件回调为基础的观察者模式模型。拥有订阅,发布,退订操作。可以满足大部分对于一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的情况。

通过事件队列依次执行,如果需要增强函数功能,只需要扩展函数即可。

另外一个观察者显而易见的例子:如果你平时使用 vue React 等框架的话,里面的 redux vuex 就涉及到观察者模式。

小结

但是,观察者虽好,可不要贪杯嗷。为什么这么说呢?

  1. 一个观察者模式的实现,难免需要很长的逻辑,难免影响一些内存
  2. 如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式是要特别注意这一点。
  3. 观察者模式可以使观察者知道所观察的对象发生了变化,但观察者模式没有使观察者知道所观察的对象是怎么发生变化的。
  4. 观察者模式让一个本可以不触发的事件,一直停留在内存中,即使事件不会执行。
  5. 弱化了对象之间的联系,会导致项目的难以跟踪维护和理解。

只要不滥用,观察者模式还是能够让逻辑分离,实现代码解耦,提高可维护程度。总比用setInterval来循环监听全局变量要好得多吧。

3. 迭代器模式

简介

迭代器模式(Iterator):提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示。

其实就是在不暴露对象的情况,可以拿到所有对象内的 元素。

其实ES6设计了Iterator的概念

下面是阮大官网介绍的栗子

实际操作

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

简单看一下代码,很清晰,十分易于理解,迭代器模式要的就是不需要关心数据结构,但只要我们调用it.next()方法,就可以知道对象所有的内容,按照顺序返回给我们!

而我们并不会暴露it对象里面所有的数据,十分人性化。

但是需要注意的是,在JavaScript的世界里,并非所有对象都有迭代器,类数组,数组,字符串是拥有迭代器的,但一个普通的对象必须部署了 Iterator 接口后才能使用。

那我们再举个栗子🌰:


var obj = {
    name: 123,
    age: 18,
    info: [{
        friend: 'abc',
        title: 'ba'
    }]
}

//

这样的一个对象,如果想能拿到所有对象,但不把对象暴露出来该怎么办呢?

这种情况对于前端开发可以说司空见惯了,后端给的数据一定是多层嵌套的,只要是 JavaScript 有的数据类型,肯定都有机会出现,那如果希望遍历取值怎么办?

相信Object.keys()是很多人的选择!

for (var key of Object.keys(obj)) {
  console.log(key + ': ' + obj[key]);
}

//name: 123
//age: 18
//info: [object Object]

小结

上面就实现了迭代器模式,很多人都认为迭代器模式比较简单,甚至很多语言都会内置迭代器,方便使用,更有甚者认为这不属于一种设计模式。

其实设计模式也就是前人总结的设计经验,建筑学中有着更多的设计模式,软件工程学中有着相比之下较少,但精华的模式,正因为它十分优秀,才会在多种编程语言下大放异彩,而不应因为它被内置而不再提及。

4. 中介者模式

中介者模式,一看名字就知道,应该是有一个东西作为两端沟通的中介。比如,我们平时租房买房,难免要和中介打交道,很多时候就被中介把买卖双方都坑了,这种时候就是个十分差劲的中介者模式实践😠!

简介

中介者模式(Mediator),用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

中介者模式的本质就是,有一个集权的管理控制函数,能够做到单向的接受信息并进行分发消息和动作。

与观察者模式的区别

这两种模式真的很像,于是网络上很容易的找到一段不明所以的话。

观察者模式,没有封装约束的单个对象,相反,观察者Observer和具体类Subject是一起配合来维护约束的,沟通是通过多个观察者和多个具体类来交互的:每个具体类通常包含多个观察者,而有时候具体类里的一个观察者也是另一个观察者的具体类。而中介者模式所做的不是简单的分发,却是扮演着维护这些约束的职责。

这句话可以说,十分晦涩难懂...

用点清楚的白话来描述二者区别:

观察者模式,肯定会有一个观察者的列表,其中可能会有增加,删除,插入,置空等等接口函数。调用观察者函数的,可以通过这些接口,进行一些操作,也就是和原有的观察者函数,共同来维护观察者列表!

中介者模式特点就是:不需要调用中介者函数的,来进行一些对中介者列表对处理。因为这份列表,是中介者函数提供的,并不需要共同维护,只需要中介者函数自己来维护。然后和观察者模式同样的,拥有分发和监听对功能!

差别也就是,具体的监听列表,能不能用调用它对函数进行维护。

实践环节

function LOL (username) {
    this.username = username
    this.task = {}
}
LOL.prototype.on = function (type, callback) {
    this.task[type].push(callback)
}
LOL.prototype.emit = function (type) {
    this.task[type].forEach(item => {
        item && item()
    })
}

var mine = new LOL('黄梵高')
mine.on('start', function(){
    console.log('start')  
})
mine.emit('start')

上面的手写代码,可以说观察者模式和中介者模式,都可以这么实现。具体区别就是观察者模式的话,实现还应该会多出unemit, empty等等函数,便于操作观察者列表。

中介者模式内部应该还会有一些其他处理,比如:

function LOL (username) {
    this.username = username
    this.task = {}
}
LOL.prototype.on = function (type, callback) {
    this.task[type].push(callback)
}
LOL.prototype.emit = function (type) {
    if (type === 'stop') { // 如果事件为停止,则把start列表全部清空
        this.task['start'] = []
    }
    this.task[type].forEach(item => {
        item && item()
    })
}

如上注释处,中介者模式会把维护列表的工作,与自己融为一体,省着你在外面操作。

中介者模式也同样可以用,买卖租房的中介来理解。无论中介是好是坑,其实作为找中介的我们,也没办法去改变它给我们提供的房屋列表。

小结

中介者模式并不困难,某种程度上和观察者实现差不多。当然二者也有区别,刚才也已经叙述。请根据业务需求具体使用。

总结

设计模式可以帮助我们设计函数结构,易于维护,开发也可以避免失误。但过度设计也会造成资源浪费,开发周期增加等缺点,所以一定要适度结合使用。在频繁改动的项目,即使你设计的十分优雅,也有可能直接被产品把功能砍掉...无论怎样抽象解耦,也一定要适度而行。比如我目前维护的项目,频繁使用观察者模式并不适合,会造成很多的资源浪费,某些情况下,甚至调整 dom 资源加载顺序也可以解决一些问题(开发时可能会有很多种不同方案,请用性价比最高的方案!)。

参考文献

Tom大叔博客

本文使用 mdnice 排版