JS设计模式

策略模式

概念

将一系列相关算法封装,并使得它们可相互替换。

简单来说:通过向封装的算法传递参数,在其封装的函数中,根据参数去执行对应的函数,达到想要的目的。

  • 可以将策略集中到一个 module 中,然后导出,再在需要的地方导入这些策略,这样就成功解耦。

示例

假如我们现在需要做 5 个判断,当判断成功时,就执行某段代码,很容易我们会想到这么做:

if (x = 1) { }
else if (x = 2) { }
else if (x = 3) { }
else if (x = 4) { }
else if (x = 5) { }

这种做法很是便捷,但是当逻辑和条件变得复杂时,这种做法就会导致难以阅读和维护,以及程序变得臃肿:

    if (x) {
        if (y) {
            if (z) {
                ...
            } else { }
        } else { }
    } else { }

这种”金字塔“编程风格属实让人难以理解。所以现在我们引入与一个最佳实践,也就是常说的设计模式:策略模式。

我们将以上代码通过策略模式,可以改成:

// Map 形式策略
// 你可以将 Map 换成对象,但是 Map 可以存储任意 key 类型。
let strategy:Mapvoid> = new Map([
    [1,(value)=>{console.log(value)}],
    [2,(value)=>{console.log(value)}],
])
let implementStrategy = (number, value) => strategy?.get?.(number)?.(value);
implementStrategy(1,'yomua'); // yomua
implementStrategy(4,99); // 99
// 对象形式策略
let strategy = {
    a(value) { console.log(value) },
    b(value) { console.log(value) },
}
const implementStrategy = (number, value) => strategy[number](value)
implementStrategy('a', 5); // 5
implementStrategy('b', 'Yomua'); // Yomua

通过这个策略模式,我们不需要进行判断,只需要在合适的情况下传入指定的参数,那么就可以达到我们的目的。

至于你说如果你想在达到某个条件时,才执行策略方法,那么你可以:在策略方法中做判断,比如:

// 对象形式策略
let strategy = {
    a(value) { value === 5 ? console.log(value) : console.log('错误的数字') },
}
const implementStrategy = (number, value) => strategy[number](value)
implementStrategy('a', 5); // 5
implementStrategy('a', 1); // 错误的数字
// 我们可以让策略更加智能些
// 接收对象,善用解构赋值
let strategy:Mapvoid> = new Map([
    [1,({value,id,...})=>{console.log(value,id,...)}],
    [2,({value,id,...})=>{console.log(value,id,...)}],
])
let implementStrategy = (number, data) => strategy?.get?.(number)?.(data);
implementStrategy(1,{value,id,....});

// 或使用剩余参数
let strategy:Mapvoid> = new Map([
    [1,(...data)=>{console.log(data)}], // data 是一个数组
    [2,(...data)=>{console.log(data)}],
]);
let implementStrategy = (number, ...data) => strategy?.get?.(number)?.(...data);
implementStrategy(1,{name:'yomua'},"yhw",4,...); 

代理模式

代理模式实现图片懒加载

不用代理实现

通常实现图片懒加载有很多种方法,现在我介绍其中一种:

  • 在一个函数中 A ,通过先在页面创建一个空的 img 元素,和 1 个虚拟的 img 元素的实例(Image 实例),然后使得该函数暴露(返回)另一个 1 个函 B 数或对象 B ;

    这个返回的函数 B 或对象 B 会把你向这里面传递的图片地址赋值给一开始创建的空 img 元素

  • 最后在你想要加载图片的任意位置,调用该函数并传递想要显示的图片的地址,就可以实现图片懒加载。
// 不用代理实现图片懒加载
    // 不用代理实现图片懒加载
    var myImage = (function () {
        var img = document.createElement('img');
        document.body.appendChild(img); // 最后在页面显示订的 img
        // 虚拟 Img:代码中存在 img 实例(元素),但页面不存在与之对应的 img 元素。
        /**
         * 拟 img,先赋给虚拟 img src,当虚拟 img 能加载成功传递过来的 src 时
         * 才使得真正的 img 渲染到页面;
         * 通过这么一层虚拟 img 可以防止浏览器渲染了无效的 src,因为即使 src 链接
         * 的地址是不存在的,浏览器也会默认渲染一个占位符,这会导致整个相关 DOM 元素
         * 更新,从而消耗浏览器性能。
         * 而如果通过虚拟 img 先进行判断你这个 src 能否成功加载,只有能成功加载,
         * 才让你渲染到页面,否则让真正的 img 永远无法加载~
         */
        var virtualImg = new Image(); 
        virtualImg.onload = () => { img.src = virtualImg.src; }
        return {setSrc(src) { virtualImg.src = src; }}
    })()
    setTimeout(() => {
        myImage.setSrc('https://pic.qqtn.com/up/2019-9/15690311636958128.jpg')
    }, 1000);

观察者模式和发布订阅模式

观察者模式

什么是观察者模式

一个称作观察者的对象,维护一组称作被观察者的对象,当被观察者发生变化时,会发送一条广播,告知所有观察它的观察者,它自身发生的任何变化,这会使得所有观察者都知道被观察者发生了什么变化。

在观察者模式中,如果观察者想要接收被观察对象的通知,则观察者必须到对应的被观察对象上注册该事件,

在以下的示例中,每一个观察者都存入到了 set 集合中,当被观察对象发生改变时(这是一个事件),将会广播通知 set 集合中所有的观察者,这样观察者们就接收到了通知。

下面让我们使用 ProxyReflect 来写一个简单的观察者模式的示例。

示例

TIP:这里的观察者是一个函数。

const set = new Set();
const observable = obj => new Proxy(obj, {
  set: function (target, key, value, receiver) {
    // 先执行默认行为得到最新数据,再通知观察者;否则观察者会观察到旧的数据,而非最新数据
    const result = Reflect.set(target, key, value, receiver);
    set.forEach(observer => observer()); 
    return result;
  },
})
const observe = func => set.add(func);
  • const set = new Set();

    这里是存放观察者的地方

  • observable

    观察某个对象,当该对象发生变化时,通知所有观察者(observe)

    • set.forEach(observer => observer());

      若观察对象发生了改变,将会发送一条广播,使得所有观察者都知道,

      并且你还可以向之传送参数,通知观察者被观察对象发生的变化或任何你想要向观察者传递的信息。

      即:使得每个在 set 中的观察者都被执行(这就相当于广播)

  • observe

    使用该函数来知道“谁”是观察。

    即:定义一个观察者,会将观察者存放到“观察者之家”: set.

    当观察者接收到被观察对象的广播时,将会执行。

// 观察一个对象
const obj = observable({
  name: '张三',
  age: 20
});

// 定义观察者(所有的观察者都会在被观察对象改变时执行,因为被观察对象发生改变时将会发送广播通知所有观察者。
observe(() => {console.log(`${obj.name}`)})
observe(() => {console.log(obj.age)})

// 改变被观察对象
obj.name = 'yomua';

/**
 * yomua
 * 20
 */

优势

观察者和发布/订阅模式鼓励人们认真考虑应用不同部分之间的关系,同时帮助我们找出这样的层,该层中包含有直接的关系,这些关系可以通过一些列的观察者和被观察者来替换掉。这中方式可以有效地将一个应用程序切割成小块,这些小块耦合度低,从而改善代码的管理,以及用于潜在的代码复用。

使用观察者模式更深层次的动机是,当我们需要维护相关对象的一致性的时候,我们可以避免对象之间的紧密耦合。例如,一个对象可以通知另外一个对象,而不需要知道这个对象的信息。

两种模式下,观察者和被观察者之间都可以存在动态关系。这提供很好的灵活性,而当我们的应用中不同的部分之间紧密耦合的时候,是很难实现这种灵活性的。

尽管这些模式并不是万能的灵丹妙药,这些模式仍然是作为最好的设计松耦合系统的工具之一,因此在任何的JavaScript 开发者的工具箱里面,都应该有这样一个重要的工具。

发布订阅模式

什么是发布订阅模式?

发布/订阅模式是观察者模式的一种变体实现,虽然它们二者很相似,但是发布订阅模式并不完全等于观察者模式。

观察者模式中,如果观察者想要接收被观察对象的通知,则观察者必须到对应的被观察对象上注册该事件,

详见:观察者模式 => 每一个观察者都存入到了 set 集合中,当被观察对象发生改变时(这相当于一个事件),将会广播通知 set 集合中所有的观察者,这样观察者们就接收到了通知。

很显然,观察者和被观察者直接形成了耦合关系,这很不利于扩展,但是这可以很好的追踪二者的关系。

所以为了解决观察者模式的耦合关系,以观察者模式为基础的变体实现:发布订阅模式就诞生了。

发布订阅模式使得发布者和订阅者之间的依赖性降低,它们之间通过一个称之为”主题/事件频道“的东西进行通信,也就说:订阅者 X,Y,Z 都订阅一个”频道 A“ 和一个 “频道 B”,发布者可以让“频道 A”或“频道 B”向订阅者 X,Y,Z 发布通知,

并且我们可以使得“频道”发布通知时,为每个订阅者发送不同的通知,传递不同的参数,让参数中包含有订阅者所需要的值(这是因为我们可以使得订阅者自定义一个函数,然后频道在发布通知时,调用该函数,并传递参数),

这样一来,发布者和订阅者之间并不会直接进行沟通,全程由频道来执行;因此你可以把“频道”看做是一个“中间商”。

因为发布订阅模式存在这种“频道”概念,当我们将这种概念具象化时,就独立出了一个合适的事件处理函数,该函数用来实现发布、订阅和取消订阅事件。

下面让我们来看看一个简单的,但却完整地实现了功能强大的 publish(), subscribe() 和 unsubscribe() 吧。

示例

  // 策略模式
  const strategy = new Map([
    [
      'isUidNumber',
      ({ uid }) => {
        if (typeof uid !== 'number') {
          console.error('uid 只能为 number 类型'); return true;
        }
      }
    ],
    [
      'isUidExist',
      ({ uid, subUid }) => {
        if (subUid < uid) { // uid 不存在中返回 true
          console.error(`指定 uid:${uid} 不存在,请重试!`); // 指定的 uid 在频道不存在(超过我们累加的 id)
          return true;
        };
      }
    ],
  ])
  const implementStrategy = (key, data) => strategy?.get?.(key)?.(data);

  // 存放发布、订阅、取消订阅的对象
  const pubsub = {};
  // 将发布、订阅、取消订阅的方法放入 pubsub
  (function (pubsub) { // 自执行函数
    // 存放所有频道
    let topics = {}, subUid = -1; // 每个订阅者的 id,该 uid 自增。
    // 发布 @topicName 频道名;@args 自定义输出的信息
    pubsub.publish = function (topicName, args) { // 使指定的频道对所有订阅者发布信息(发布的行为由订阅者自己设置)
      if (!topics[topicName]) { // 不存在的频道发布了信息
        console.error(`发布失败,请先订阅频道 ${topicName}`)
        return false;
      }
      if (topics[topicName].length === 0) { // 频道存在但没有任何订阅者,不需要发布信息
        console.error(`发布失败,频道 ${topicName} 不存在订阅者`)
        return false;
      }
      const subscribers = topics[topicName]; // 得到指定频道中的所有订阅者
      const len = subscribers ? subscribers.length : 0; // 得到当前订阅的频道中存在多少订阅者
      // 发布一个频道的信息时,通知它的所有订阅者,从最后一个订阅的人开始通知
      // len 为 0 时还会进行最后一次循环,因为最后 1-- 时,会先用 1 来判断,最后再减
      // 如果是 --len,那么会先减,然后用 0 判断,就导致循环体内部的 len 不会为 0
      while (len--) {
        console.log(`${topicName} 频道向所有订阅者发布信息,传递的参数:'${topicName}'' 和 '${args}'`)
        subscribers[len].func(topicName, args); // 调用订阅者自定义的函数并向之传入需要的参数
      }
      return this;
    };

    /** 订阅
     * 将每个订阅者存储到 topics 对象中,每一个订阅者都是一个数组,数组中存一个对象,具有 func 和 uid 属性
     * @func. 频道对订阅者如何发布信息,由用户自定义,传递的参数则由频道来决定(发布者决定)
        (topicName,data)=>{},该函数接收【频道名】和【发布的内容】 作为参数
     * @uid 订阅者的 id,用来标识订阅者
     */
    pubsub.subscribe = function (topicName, func) { // 为指定的频道名添加订阅者
      if (!topics[topicName]) { topics[topicName] = []; }; // 若当前不存在该频道,则初始化频道
      let uid = (++subUid); // 对 uid 进行累加
      topics[topicName].push({ uid, func, }); // 向对应的频道存放每个订阅者的信息:uid 和 自定义的行为
      console.log('当前总共存在的订阅者:')
      console.log(topics)
      return uid;
    };

    // 取消订阅
    pubsub.unsubscribe = function (uid) { // 根据指定的订阅者的 uid 来使得订阅者取消对频道的订阅
      // id 类型非 number 且 uid 不存在,则报错
      if (
          implementStrategy('isUidNumber', { uid }) || 
          implementStrategy('isUidExist', { uid, subUid })
      ) return false;
      console.log(`即将移除 uid:${uid} 的订阅者`)
      for (let topicName in topics) { // 遍历每个频道
        let topicArr = topics[topicName]; // 得到当前频道的所有订阅者
        if (topicArr?.length === 0) { // 当前频道不存在订阅者 
          console.error('频道' + topicName + '不存在订阅者或不存在该频道')
          return false;
        }
        for (let i = 0, j = topicArr?.length; i < j; i++) { // 遍历当前频道中的所有订阅者
          if (topicArr[i].uid === uid) { // 查找频道中的订阅者 uid 和指定 uid 相同的项
            topicArr.splice(i, 1); // 将当前订阅者从频道中移除
            // Reflect.deleteProperty(topics, m) // 移除整个频道
            console.log('移除成功')
            console.log('移除后,现有订阅如下:')
            console.log(topics)
            return uid;
          }
        }
      }
      return this;
    };
  }(pubsub));

以上代码将“频道”这一概念进行具象化实现,虽然简单,但是功能却完整,

具有:订阅、取消订阅和发布功能,下面让我们来使用它吧:

  console.log('%c----------订阅频道 cctv1 ----------', 'color:red')
  // 频道发布消息时,将执行由用户定义的发布行为。,该函数接收【频道名】和【要发布的内容】
  const cctv1OneFunc = (topciName, data) => { console.log(`我是订阅者 cctv1One 的函数`) }
  const cctv1TwoFunc = (topciName, data) => { console.log(`我是订阅者 cctv1Two 的函数`) }
  // 订阅频道 cctv1,且用一个 callback 接收频道的信息,频道发布时会传递:当前频道名和一个其他自定义信息。
  let cctv1One = pubsub.subscribe("cctv1", cctv1OneFunc);
  let cctv1Two = pubsub.subscribe("cctv1", cctv1TwoFunc);
  // @arg1:要发布信息的频道, @arg2:发布的信息内容,频道对所有订阅者发布消息时,会执行订阅者自定义的函数。
  pubsub.publish('cctv1', 'cctv1 发布信息'); 


  console.log('%c----------订阅频道 cctv2 ----------', 'color:red')
  const cctv2OneFunc = (topciName, data) => { console.log(`我是订阅者 cctv2One 的函数`) }
  let cctv2One = pubsub.subscribe("cctv2", cctv2OneFunc);
  pubsub.publish('cctv2', 'cctv2 你好!')


  console.log('%c----------取消订阅  ----------', 'color:red')
  pubsub.unsubscribe('2'); // error,uid 类型错误
  pubsub.unsubscribe(2); // okay,uid:2 的订阅者取消订阅
  pubsub.unsubscribe(2); // error,频道不存在订阅(早已取消)
  pubsub.unsubscribe(22); // error,uid:22 的订阅者不存在

发布订阅模式的优势和缺陷

优势

发布者和订阅者之间没有直接依赖关系,在后面代码进行扩展或维护时,将带来便利。

观察者和发布/订阅模式都鼓励人们认真考虑应用不同部分之间的关系,同时帮助我们找出这样的层,该层中包含有直接的关系,

这些关系可以通过一些列的观察者和被观察者来替换掉。这中方式可以有效地将一个应用程序切割成小块,这些小块耦合度低,从而改善代码的管理,以及用于潜在的代码复用。

两种模式下,观察者和被观察者之间都可以存在动态关系。这提供很好的灵活性,而当我们的应用中不同的部分之间紧密耦合的时候,是很难实现这种灵活性的。

尽管这些模式并不是万能的灵丹妙药,这些模式仍然是作为最好的设计松耦合系统的工具之一,因此在任何的JavaScript 开发者的工具箱里面,都应该有这样一个重要的工具。

缺陷

事实上,发布订阅模式的一些问题实际上正是来自于它所带来的一些好处。

在发布/订阅模式中,将发布者和订阅者解耦,将会在一些情况下,导致很难确保我们应用中的特定部分按照我们预期的那样正常工作;例如,发布者可以假设有一个或者多个订阅者正在监听它们。

比如我们基于这样的假设,在某些应用处理过程中来记录或者输出错误日志。如果订阅者执行日志功能崩溃了(或者因为某些原因不能正常工作),因为系统本身的解耦本质,发布者没有办法感知到这些事情。

另外一个这种模式的缺点是,订阅者对彼此之间存在没有感知,对切换发布者的代价无从得知。因为订阅者和发布者之间的动态关系,更新依赖也很能去追踪。

观察者模式和发布订阅模式的相同点和区别

相同点

两个模式都有通信的双方,观察者和被观察者,发布者和订阅者。

区别

观察者模式中,观察者和被观察者是直接进行耦合,进行互相访问的。

而发布订阅模式中,存在“主题/事件频道”概念,“频道”来对信息进行过滤,执行发布命令的人(发布者)需要通过频道对订阅者发布信息,而订阅者需要去订阅“频道”才能接受发布者发布的信息。

这样发布者和订阅它们双方都不知道对方的存在,都是通过“频道”进行交流,互相依赖性低。

单例模式

什么是单例模式

单例模式也称之为单体模式,规定一个类只有一个实例,并且提供可全局访问点。

所谓的一个类只能有一个模式,即:不管使用 new 对某个类进行几次实例化,所返回的得到的结果都只能是相同的,即:第一次实例化时所得到的对象。

或许你可能没有听过单例模式在这个名词,但是我相信,你肯定在日常编码中h使用过它,这是因为单例模式的特点是:“全局”和“唯一”,那么我们可以联想到 JavaScript 中的全局对象。

利用 ES6 的 let 或 const 不允许重复声明的特性,刚好符合这两个特点;是的,全局对象是最简单的单例模式;

let obj = {
    name:"yomua",
    getName:function(){}, // 提供一个函数,可以通过该函数访问到 obj 内部的变量
}

示例

简单版单例模式

分析:只能有一个实例,所以我们需要使用if分支来判断,如果已经存在就直接返回,如果不存在就新建一个实例;

let Singleton = function(name){ // 创建一个“类”
    this.name = name;
    this.instance = null; 
}
Singleton.prototype.getName = function(){console.log(this.name);} // 输出实例化“类”时传递的参数
Singleton.getInstance = function(name){
    if(this.instace){return this.instance;} // 使得只存在一个 Singleton 实例
    return this.instance = new Singleton(name);
}

let winner = Singleton.getInstance("winner"); 
winner.getName(); //winner 
let sunner = Singleton.getInstance("sunner"); // 即使多次实例化 Singleton,都会得到第一次实例化的结果
sunner.getName(); //winner

上面代码中我们是通过一个变量 instance 的值来进行判断是否已存在实例,如果存在就直接返回 this.instance,如果不存在,就新建实例并赋值给 instance,这就保证了永远只会存在一个 Singleton 的实例。

但是上面的代码还是存在问题,因为创建对象的操作和判断实例的操作耦合在一起,并不符合”单一职责原则“;

改良版:

思路:通过一个闭包,来实现判断实例的操作;

let CreateSingleton = (function(){
    let instance = null;
    return function(name){
        this.name = name;
        if(instance){return instance}
        return instance = this;
    }
})()
CreateSingleton.prototype.getName = function(){console.log(this.name);}
let winner = new CreateSingleton("winner"); 
winner.getName(); //winner
let sunner = new CreateSingleton("sunner");  
winner.getName();

上面改良的单例模式中,我们通过闭包(在这里是:一个自执行函数返回另一个函数)将用来判断是否已经实例化过的变量和创建实例分开(一开始它们二者都存在于一个函数中,使得这个函数具有两个功能,这不符合”单一职责原则“。

中介者模式

什么是中介者模式

字典中,中介者的定义是:一个中立方,在谈判和冲突解决过程中起辅助作用。

在程序设计模式中:一个中介者是一个行为设计模式,使我们可以导出统一的接口,这样系统不同部分就可以彼此通信。

比如:一个系统中存在大量组件,组件之间需要互相通信,那么我们可以通过中介者模式创建一个中介者并暴露出一个统一的接口,使得每个组件可以通过这个接口去访问其他组件,而非组件和组件进行直接通信。

这和发布/订阅模式很相像,因为发布/订阅模式中也存在着一个“中介”,该中介使得发布者和订阅者必须通过它来通信,而非直接进行交互,它们二者的比较详见:中介者模式 VS 发布订阅模式

优势和缺点

优势

能帮助我们对组件/对象之间进行解耦,改善组件的重用性,并使得整个系统维护成本降低。

缺点

中介者模式的缺点正是由于其优势带来的(发布订阅模式也是如此),对组件/对象之间的通信进行解耦,使得它们通过一个中介者对象进行互相通信,势必会让中介者对象变得庞大、臃肿,

且中介者对象本身就是难以维护的,并且由于中介者模式会新增一个对象,这会带来内存上的开销,性能也会降低,毕竟对象和对象之间的直接访问肯定 比 先访问一个中间商然后才去访问其他对象快得多。

示例

小游戏

// 中介者模式案例:泡泡堂(引入中介者)
let playerDirector = (function () {
  let players = {}; // 存放所有玩家
  let operations = { // 控制玩家状态
    addPlayer: function (player) {
      let teamColor = player.teamColor;
      if (!players[teamColor]) { players[teamColor] = []; }
      players[teamColor].push(player)
    },
    removePlayer: function (player) {
      let teamColor = player.teamColor;
      let teamPlayers = players[teamColor] || [];
      for (let index = 0, len = teamPlayers.length; index < len; index++) {
        if (teamPlayers[index] == player) { teamPlayers.splice(index, 1); break; }
      }
    },
    changeTeam: function (player, newTeamColor) {
      operations.removePlayer(player);
      player.teamColor = newTeamColor;
      operations.addPlayer(player);
    },
    playerDead: function (player) {
      let teamColor = player.teamColor;
      let teamPlayers = players[teamColor];
      let allDead = true;
      player.state = 'dead';
      for (let index = 0, len = teamPlayers.length; index < len; index++) {
        if (teamPlayers[index].state != 'dead') { allDead = false; break; }
      }
      if (allDead) {
        for (let index = 0, len = teamPlayers.length; index < len; index++) {
          teamPlayers[index].lose();
        }
        for (let color in players) {
          if (color != teamColor) {
            for (let index = 0, len = players[color].length; index < len; index++) {
              players[color][index].win();
            }
          }
        }
      }
    }
  };
  // 玩家状态改变(死亡/改变对象等)就执行对应的操作
  let ReceiveMessage = function () {
    // 以下两个 arguments 指的是玩家的属性:{name,state,teamColor,_proto_}
    let message = Array.prototype.shift.call(arguments); // 得到当前执行的操作,如:addPlayer。
    operations[message].apply(this, arguments); // 执行对应的方法并传入 arguments(玩家的属性)
  }
  return { ReceiveMessage }; // 返回一个对象
})()
// 定义玩家属性和公共行为
let Player = function (name, teamColor) {
  this.name = name;
  this.teamColor = teamColor;
  this.state = 'live';
}
Player.prototype.win = function () { console.log(this.name + '胜利了'); }
Player.prototype.lose = function () { console.log(this.name + '失败了'); }
Player.prototype.remove = function () {
  console.log(this.name + '掉线了');
  playerDirector.ReceiveMessage('removePlayer', this);
}
Player.prototype.die = function () {
  console.log(this.name + '死亡');
  playerDirector.ReceiveMessage('playerDead', this);
}
Player.prototype.changeTeam = function (color) {
  console.log(this.name + '换队');
  playerDirector.ReceiveMessage('changeTeam', this, color);
}

// 以工厂模式创建新玩家
let playerFactory = function (name, teamColor) {
  let newPlayer = new Player(name, teamColor);
  playerDirector.ReceiveMessage('addPlayer', newPlayer);
  return newPlayer;
}
// 红队
let player1 = playerFactory('张三', 'red'),
  player2 = playerFactory('张四', 'red'),
  player3 = playerFactory('张五', 'red'),
  player4 = playerFactory('张六', 'red');
// 蓝队
let player5 = playerFactory('辰大', 'blue'),
  player6 = playerFactory('辰二', 'blue'),
  player7 = playerFactory('辰三', 'blue'),
  player8 = playerFactory('辰四', 'blue');

/** 开始比赛 */
// 掉线
// 依次输出:张三掉线了 张四掉线了
player1.remove();
player2.remove();

// 更换队伍
// 依次输出:张五换队 张五死亡
player3.changeTeam('blue');

// 阵亡
// 依次输出:辰大死亡 辰二死亡  辰三死亡 辰四死亡
// 辰大失败了 辰二失败了  辰三失败了 辰四失败了 张五失败了
// 张六胜利了
player3.die();
player5.die();
player6.die();
player7.die();
player8.die();

类似发布订阅模式的中介者模式示例

let mediator = (function () {
  //存储可以广播或收听的 topic
  let topics = {};
  // 订阅一个 topic,提供一个 callback,当指定的 topic 需要被广播时,调用该 callback
  let subscribe = function (topic, fn) {
    if (!topics[topic]) { topics[topic] = []; }
    topics[topic].push({ context: this, callback: fn });
    return this;
  };
  //向应用程序的其余部分发布/广播事件
  let publish = function (topic) {
    let args;
    if (!topics[topic]) { return false; }
    args = Array.prototype.slice.call(arguments, 1);
    for (let i = 0, l = topics[topic].length; i < l; i++) {
      let subscription = topics[topic][i];
      subscription.callback.apply(subscription.context, args);
    }
    return this;
  };
  return {
    publish: publish,
    subscribe: subscribe,
    installTo: function (obj) {
      obj.subscribe = subscribe;
      obj.publish = publish;
    }
  };
}());

如果你仔细查看发布订阅模式中的示例,你会发现,本示例只是把变量名字由 pubsub 换成了 mediator 而已。

因为这两个模式在某种程度上,可以说实现都是类似的,只不过侧重点不同。

中介者模式 VS 发布订阅模式

开发人员往往不知道中介者模式和发布/订阅模式之间的区别。

不可否认,这两种模式之间有一点点重叠,如:它们都通过”中介“使得对象和对象之间进行解耦,但是它们之间还是有不同点的。

让我们来回顾一下发布订阅模式的特点:

它定义了发布者和订阅者之间的依赖关系或许是一对多(一个订阅者订阅多个频道),又或者是多对一(多个订阅者订阅一个频道)、多对多(多个订阅者订阅多个频道)等,当发布者通过频道发布通知时,该频道下所有的订阅者都会接收到该通知。

而中介者模式主要作用是:用一个中介对象来封装一系列的对象交互,使得对象和对象之间解耦。

显然的,发布订阅模式强调的是统一通讯这一概念,而中介者模式强调是对象和对象之间交互,而非统一通讯。

工厂模式

在中介者模式的示例小游戏中我们有使用到工厂模式,现在让我们来看看你什么是工厂模式吧。

什么是工厂模式

工厂模式是一种关注对象创建概念的创建模式。它旨在暴露出一个公共接口,且使用该公共接口去指定我们想要创建的对象的类型,这样我们就避免了通过直接使用 new 运算符去创建对象。

比如:我们需要创建一种类型的 UI 组件,在不使用工厂模式时,可以这么做:用一个函数去描述该组件,每次想要创建一个新的组件就实例化(new)该函数,这样我们就得到了不同属性但同类型的组件;

而工厂可以在定义一个函数,在该函数内部去实例化这个函数组件,想要创建新组件,只需要调用该函数并传入合适的参数即可;这个函数就像一个工厂,它内部的“流水线”非暴露的,我们交予一个东西给工厂,工厂就会生产出我们需要的东西。

示例

// 每种车辆类型的属性
function Car(options) {
  this.doors = options.doors || 4;
  this.state = options.state || "brand new";
  this.color = options.color || "silver";
}

function Truck(options) {
  this.state = options.state || "used";
  this.wheelSize = options.wheelSize || "large";
  this.color = options.color || "blue";
}

// 定义工厂、如何使用工厂生产的方法
function VehicleFactory() { }
VehicleFactory.prototype.createVehicle = function (options) {
  if (options.vehicleType === "car") {this.vehicleClass = Car;} 
      else {this.vehicleClass = Truck;}
  return new this.vehicleClass(options);
};

var carFactory = new VehicleFactory(); // 得到工厂

var car = carFactory.createVehicle({ // 让工厂帮我们创建需要的类型为 car 的车
  vehicleType: "car",
  color: "yellow",
  doors: 6
});

var truck = carFactory.createVehicle({  // 修改工厂创建的车辆类型为:truck
  vehicleType: "truck",
  state: "like new",
  color: "red",
  wheelSize: "small"
});

// 得到车辆信息
console.log(car);
console.log(truck);

不难看出,使用工厂模式能让目的很容易被得知,如果在以上示例中,直接使用 new Car/Truck(options) 的方式固然也能得到一样的结果,但是一旦当车辆类型变多时,复数个 new XX 会给维护人员或者几个月后的你带来疑惑,

且这也并不容易使得该程序进一步扩展,比如:我们创建车辆时,需要判断当前车辆的颜色是否为 "yellow" 才创建,那么我们需要在复数个类似 Car 这样的函数中去添加判断才能完成,

但是如果基于以上示例,我们只需要在工厂中判断 Options.color 是否为 "yellow" 即可,诸如此类还有很多,就不一一列举了。

工厂模式的适用情况

当被应用到下面的场景中时,工厂模式特别有用:

  • 当我们的对象或者组件设置涉及到高程度级别的复杂度时。
  • 当我们需要根据我们所在的环境方便的生成不同对象的实体时。
  • 当我们在许多共享同一个属性的许多小型对象或组件上工作时。
  • 当带有其它仅仅需要满足一种API约定(又名鸭式类型)的对象的组合对象工作时.这对于解耦来说是有用的。

何时不要去使用工厂模式:

当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性。

除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。

由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

抽象工厂

什么是抽象工厂

抽象工厂指的是:把一组独立的工厂封装在一起的工厂。

抽象工厂的目标:以一个通用的目标将一组独立的工厂进行封装。

以上二者表明抽象工厂仍是工厂模式的一种。

抽象工厂的适用

抽象工厂应该被用在一种必须从其创建或生成对象的方式处独立,或者需要同多种类型的对象一起工作这样的系统中。

示例

// 定义车辆以及车辆默认属性
function Car(options) {
  this.doors = options.doors || 4;
  this.state = options.state || "brand new";
  this.color = options.color || "silver";
}

function Truck(options) {
  this.state = options.state || "used";
  this.wheelSize = options.wheelSize || "large";
  this.color = options.color || "blue";
}

// 一个对象工厂,用来注册、创建、得到车辆
var AbstractVehicleFactory = (function () {
  var typesObj = {};
  return {
    getVehicle: function (type, customizations) {
      var Vehicle = typesObj[type];
      return (Vehicle ? new Vehicle(customizations) : null);
    },
    registerVehicle: function (type, VehicleFunc) {
      var proto = VehicleFunc.prototype;
      if (proto.contract) typesObj[type] = VehicleFunc;  // 仅仅注册有合同的车辆
      return AbstractVehicleFactory;
    },
    registerContract: function (type) {
      if (typeof type !== 'function') { console.log('应传入一个车辆类型'); return; }
      console.log(type.name === 'Car')
      if (type.name === 'Car') { Car.prototype.contract = true }
      if (type.name === 'Truck') { Truck.prototype.contract = true }
    }
  };
})();

// 为车注册合同
AbstractVehicleFactory.registerContract(Car);
// 注册车辆
AbstractVehicleFactory.registerVehicle("car", Car);
AbstractVehicleFactory.registerVehicle("truck", Truck);

// 车辆属性
var car = AbstractVehicleFactory.getVehicle("car", {
  color: "lime green",
  state: "like new",
});

var truck = AbstractVehicleFactory.getVehicle("truck", {
  wheelSize: "medium",
  color: "neon yellow"
});

console.log(car); // {doors: 4, state: "like new", color: "lime green"}
console.log(truck); // null

以上示例中,AbstractVehicleFactory 对象可以认为是一个抽象工厂,它将三个独立的目标进行组装,从而得到它。

当然,你或许认为这三个函数它们是一个工厂中的,而 AbstractVehicleFactory 并非抽象工厂,只是一个普通的工厂,这样理解也不是不行,毕竟抽象工厂就是普通工厂模式衍生过来的。

原型模式

什么是原型模式

原型模式指的是:通过克隆的方式,基于一个现有对象的模板创建对象的模式。

即:以现有对象作为蓝图,从而创建新对象的模式。

你可以把原型模式认为是一种基于原型(Prototype)的继承。

使用原型模式的好处

使用原型模式的好处之一就是,我们在 JavaScript 提供的原生能力之上工作的,而不是 JavaScript 试图模仿的其它语言的特性。

且该模式还会带来一些性能上的提升:当为”蓝图“(基对象)定义方法时,这些方法都是使用引用(对象.方法名)创建的,这会使得基于蓝图创建的子对象,都会指向同一个方法(蓝图中的方法),而不是子对象单独创建一个该函数的拷贝,

那么这样,自然就会带来性能上的提升。

示例

使用 Object.create() 应用原型模式

var vehicle = {
  getModel: function () {
    console.log(this); // 指的是 car 对象:{age: "21", name: "yomua"}
    console.log(this.age); // 21
  }
};
var car = Object.create(vehicle, {
  "age": {value: '21',enumerable: true}, // 这里的 age 值是:21,并非是一个对象
  "name": {value: "yomua",enumerable: true }
});
car.getModel(); // 21
console.log('getModel' in car); // true

使用 Object.create() 去实现原型模式是很容易的一件事情,因为该方法本身就创建了一个拥有特定原型的对象,并且还可以为指定的对象添加属性和其描述,如:

Object.create(proto[,propertiesObject])

  • proto:以该对象作为创建的新对象的原型
  • propertiesObject:要添加到新创建对象的可枚举属性(属于自身,而非属于原型链上的),默认为 undefined.
  • 返回值:一个新对象

不使用 Object.create() 应用原型模式

即使不使用 Object.create() 我们也能模拟原型模式:

var vehiclePrototype = {
  init(carName) { this.name = carName; },
  getModel() { console.log("汽车名:" + this.name); } // 汽车名:yomua
};

// 工厂模式,用一个工厂去帮我们把原型添加到函数上,并初始化车辆名字。
function vehicleFactory(carName) {
  function Car() { }
  Car.prototype = vehiclePrototype;
  const car = new Car();
  car.init(carName);
  return car;
}
const car = vehicleFactory('yomua')
car.getModel(); // 汽车名:yomua

通过以上示例,不难发现:如果不使用类似 Object.create() 这样的方法直接为某个对象创建原型,那么就只好先创建一个函数,为该函数创建原型,再实例化该函数得到其实例(对象),最后在用该函数的实例获取原型上的方法/属性了。

或者你能再简单点:

const car = (function (){
    function Car(){};
    Car.prototype = vehiclePrototype;
    return new Car();
})();
car.init('yomua');
car.getModel(); // 汽车名:yomua

本节中通过为函数添加原型去应用原型模式的这种方法,实际上不算正宗的原型模式,因为原型模式只是将一个对象链接到另一个对象,并没有其他概念,

而这里使用的方法很明显的多创建了一个函数,不过即使如此,但本节中使用的方法在一些情况下比直接使用 Object.create() 要更甚一筹。

命令模式

什么是命令模式

命令模式就是将请求或者操作封装到一个单独的对象中,使得请求/操作可以进行参数的传递,并以函数的形式被执行。

另外命令模式使得我们可以将【对象实现的行为】以及【对这些行为的调用】进行解耦。

命令模式的理念:将执行对象的某个行为这样的责任,从对象上分离,取而代之的是将这种责任委托给其他对象或是让当前对象的某个行为去发出命令。

语义更加清楚,令维护人员和开发人员神清气爽。

示例

let CarManager = {
  requestInfo(model, id) { return `${model},${id}`; },
  buyVehicle(model, id) { return `${model},${id}`; },
  arrangeViewing(model, id) { return `${model},${id}`; }
};

以上是将三个相关的行为封装到一个对象中,通常如果我们想要使用这些行为,是这么做的:CarManager.xxx(…)

当然,这样做无可厚非,这段 JS 代码并没有做错什么,但是没有做错什么并不代表就做好了,为什么?

比如:想象如果 CarManager 的核心 API 会发生改变的这种情况;这可能需要所有直接访问这些方法的对象也跟着被修改。

而这可以被看成是一种耦合,明显违背了 OOP 方法学尽量实现松耦合的理念,取而代之,我们可以通过更深入的抽象这些 API 来解决这个问题。

让我们来加一个命令,让该命令负责执行某对象的某行为:

const run = (thisArg, funcName, ...rests) => thisArg[funcName].apply(thisArg, rests);;
// 或
CarManager.execute = function (funcName) { // 将命令作为该对象的行为,让这个行为负责执行其他行为
    return this[funcName] && this[funcName].apply(CarManager, [].slice.call(arguments, 1))
}

这样,我们可以使用 run 命令,使用如下的形式,来执行指定对象的指定行为:

run(CarManager, 'arrangeViewing', '小丑模式', '9999'); // model:小丑模式,id:9999
...
// 或
CarManager.execute('arrangeViewing', '小丑模式', '9999'); // model:小丑模式,id:9999
...

命令模式的优势和缺点

优势

在示例中,我们应用了命令模式,那么这样做有什么好处呢?

其实最容易让人发掘的好处就是:这种以命令形式执行的行为,其语义和美观程度上更好

同时也降低了系统耦合度——对象直接访问行为变成了通过命令控制访问哪个对象的行为。

就是说:使用者其实其实并不需要关注该行为是如何被调用的或者说对象是如何调用该行为的,使用者只需要知道:这个命令可以帮你执行对象的行为,以及你可以向其行为中传递一些参数来决定行为的模式。

所以这就是让对象和行为进行解耦。

缺点

使用命令模式可能会导致某些系统有过多的具体命令类。

使用场景

在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。

在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。

即:我们可以应用命令模式:通过调用者调用接受者执行命令,顺序:调用者→命令→接受者

笔者注:这里的调用者指:存储一系列命令的对象,通过该对象调用命令,使得接受者执行。

总结

不管是何种模式,都不是万能药,每种模式没有高下之分,只要在合适的地方运用合适的模式/对模式进行组合使用,就能将模式的威力最大化,使得程序异常健壮。

且从本文章来看,以上的设计模式的核心目的只有一个:解耦。

Reference

你可能感兴趣的:(JS设计模式)