JavaScript常用设计模式整理总结

137 阅读16分钟

1.单例模式


单例模式定义 : 保证一个类仅有一个实例,并提供一个全局的访问点。

实现思路 : 使用一个变量作为标志,判断当前是否已经为某个类创建过对象,如果已经创建过,则直接返回已经创建的对象,如果没有,则进行创建对象。

应用场景:
1.单例模式的应用范围还是很广的,比如需要一个登录浮窗,而这个登录浮窗是唯一的,只会被创建一次,那么这个登录浮窗适合用单例模式创建。
2.全局变量等。
3.jquery中的$。
4.vuex中的store 等等。都是单例的应用。

首先,来看一个基本的单例模式

var Singleton = function(name) {
  this.name = name;
};

Singleton.prototype.getName = function() {
  console.log(this.name);
};

Singleton.getInstance = (function() {
  var instance = null;
  return function(name) {
    if (!instance) {
      instance = new Singleton(name);
    }
    return instance;
  };
})();

var a = Singleton.getInstance('xuz');
var b = Singleton.getInstance('ddd');

console.log(a); // 输出 Singleton  {name: 'xuz'}
console.log(b); // 输出 Singleton {name: 'xuz}
console.log(a === b); // true

基于es6的语法及代理,实现单例模式

// es6 people类
 class people {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}
//getSingleton 是抽离的管理单例的逻辑代码,需要创建单例的对象fn 当成参数动态的传入getSingleton函数中。
const getSingleton = (() => {
  let instance = null;
  return fn => instance || (instance = fn);
})();

/*
*比如想到得到单例的类people的对象
*/
let a = getSingleton(new people('xzc'));
let b = getSingleton(new people('dad'));

console.log(a); // people { name: 'xzc' }
console.log(b); // people { name: 'xzc' }
console.log(a === b); // true

/* 
或者 需要创建唯一的对象,对象创建方法 唯一性
*/
const func1 = function() {
  console.log(2222);
};
const func2 = function() {
  console.log(4444);
};
let e = getSingleton(func1);
let f = getSingleton(func2);
e(); // 2222
f(); // 2222
console.log(e === f); // true

上述就是单例模式的实现,实现过程中有闭包和高阶函数的概念,不懂得同学可以先看看这部分知识。

2.策略模式


策略模式定义 : 定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。

策略模式优点: 策略模式易于理解,扩展,切换等,策略模式可以有效的避免多重条件选择语句,并且策略模式的算法可以复用在系统的其他地方,从而避免许多重复的复制粘贴。

策略模式使用场景: 策略模式的定义是封装算法的, 比如,年终计算年终奖时候,等级A,B,C,D 分别有不同的计算公式,此时可以用策略模式。但我们也可以把算法的含义扩散开来,使策略模式也可以封装一系列的 ‘业务规则’ ,只要这些业务的目标一致,并且可替换。例如,我们可以用策略模式来 检验表单各项的输入是否合法等。

说这么多,来看个场景案例:我们需要编写一个计算员工年终奖的函数,函数需要传入员工的等级和工资数额

function calculateMoney(level, baseMoney) {
  // A等级系数为1.3
  if (level === 'A') {
    return baseMoney * 1.3;
  }
  if (level === 'B') {
    return baseMoney * 1.1;
  }
  if (level === 'C') {
    return baseMoney * 1;
  }
  if (level === 'D') {
    return baseMoney * 0.9;
  }
}

calculateMoney('A', 10000); // 13000
calculateMoney('D', 10000); // 9000


上段代码是容易想到的代码,但有着明显的缺点:

  • if判断语句较多
  • 缺乏弹性,比如需要增加E绩效,或者修改A绩效的系数,必须深入calculateMoney内部去实现,违反开放-封闭原则
  • 算法的复用性比较差,不能在其他地方复用奖金的算法

使用策略模式重构代码:

const A = baseMoney => baseMoney * 1.4;
const B = baseMoney => baseMoney * 1.1;
const C = baseMoney => baseMoney * 1;
const D = baseMoney => baseMoney * 0.9;

const calculate = (fn, money) => fn(money);

console.log(calculate(A, 10000));  // 14000
console.log(calculate(D, 10000));  // 9000


如果不是特意强调,你可能也认不出来这是个策略模式的实现。

3.代理模式


代理模式定义 : 为一个对象提供一个代用品,或者占位符,以便控制对它的访问。
可能定义很生硬,举个例子解释下,小明想追求小芳,于是就想着送朵花给小芳表白,小芳心情好的时候收到花,成功率大,小芳心情不好的时候,收到花表白成功率小。但小明不知道小芳什么时候心情好,心情差,但小芳的闺蜜小兰知道。于是小明就把花转交给小兰,让小兰在小芳心情好的时候送花。 这里的小兰就相当于代理。

代理模式使用场景: 代理模式有很多小分类,但在javascript中最常用的就是虚拟代理和缓存代理。
虚拟代理: 虚拟代理就是将一些开销很大的对象,延迟到需要他的时候再执行。
缓存代理:为一些开销比较大的对象结果提供暂时的存储,下次再请求时,直接返回存储的结果。

虚拟代理

场景: 图片预加载。如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往是一篇空白,常用的做法就是先用图片或者文字占位,然后用异步的方法加载图片,等图片加载好了再把它填充到img节点里面。

首先先看没使用代理实现的图片预加载。

//MyImg除了负责给img节点设置src,还负责预加载图片。
 var MyImg = (function() {
      var imgNode = document.createElement('img');
      document.body.appendChild(imgNode);

      var img = new Image();
      img.onload = function() {
        imgNode.src = img.src;
      };

      return {
        setUrl: function(src) {
          imgNode.src = './1.jpg';
          img.src = src;
        }
      };
    })();

    MyImg.setUrl('http://pic44.nipic.com/20140723/18505720_094503373000_2.jpg');

代理实现图片预加载

  //MyImg只负责给节点设置src
    const MyImg = (() => {
      const imgNode = document.createElement('img');
      document.body.appendChild(imgNode);
      return src => (imgNode.src = src);
    })();
    
    //保证代理和本体接口的一致性。
    const MyImgProxy = (() => {
      const img = new Image();
      img.onload = () => {
        MyImg(img.src);
      };
      return src => {
        MyImg('./1.jpg');
        img.src = src;
      };
    })();

    MyImgProxy('http://pic44.nipic.com/20140723/18505720_094503373000_2.jpg');

加入代理后,降低了耦合性,满足了单一职责原则(对象和函数等,应该仅有一个引起它变化的原因)。即便以后不需要预加载了,仅需要改成请求本体而不是请求代理对象即可,而不是再MyImg里面修改代码。

缓存代理

场景: 计算乘积。

没有代理,每次都进行计算

const mult = (...rest) => {
  console.log('开始计算了');

  let a = 1;
  for (let index = 0; index < rest.length; index++) {
    a = a * rest[index];
  }
  return '结果是' + a;
};
console.log(mult(1, 2, 3)); // 开始计算了 结果是6
console.log(mult(1, 2, 3)); // 开始计算了 结果是6

加了代理,可以看出相同的参数,直接取得缓存的值,没有重复计算

const mult = (...rest) => {
  console.log('开始计算了');

  let a = 1;
  for (let index = 0; index < rest.length; index++) {
    a = a * rest[index];
  }
  return '结果是' + a;
};

//加入代理。

const proxyFactory = fn => {
  const cache = {};
  return (...rest) => {
    const value = rest.join(',');
    if (value in cache) {
      return cache[value];
    }
    return (cache[value] = fn(...rest));
  };
};

const mult1 = proxyFactory(mult);
console.log(mult1(1, 2, 3));// 开始计算了 结果是6
console.log(mult1(1, 2, 3));// 结果是6

tips: 在编写业务代码得时候,不需要猜测是否需要使用代理,当真正发现不方便直接访问某个对象得时候,再编写代理也不迟。

4.观察者模式


观察者模式定义 : 它定义对象间得一对多的依赖关系,当一个对象的状态发生改变的时候,所有依赖于它的对象都将得到通知。在js中,我们一般使用事件模型来代替观察者模式。

观察者模式使用场景: 观察者模式在js中的应用范围还是很广的。浏览器的事件都是该模式的实现,还有nodejs中event事件等。

这里,我们实现一个简单的nodejs的eventEmitter,来理解下观察者模式

class eventEmitter {
  constructor() {
    this.events = {}; // 用来存放所有的注册事件
  }

  //订阅事件
  addEventListener(type, fn) {
    if (!this.events[type]) {
      this.events[type] = []; //当前没有监听事件,则置为空数组
    }
    this.events[type].push(fn); //当需要添加的监听事件 添加到数组
  }

  //发布事件
  emit(type, ...rest) {
    const fns = this.events[type];
    if (!fns || fns.length === 0) {
      return false; //事件为空,直接返回
    } else {
      fns.forEach(fn => {
        //依次执行事件的函数
        fn(...rest);
      });
    }
  }

  removeEventListener(type, fn) {
    this.events[type].splice(this.events[type].indexOf(fn), 1); //取消订阅
  }
}

const event = new eventEmitter();
event.addEventListener('xzc', (a, b) => {
  console.log('先添加xzc' + a + b);
});
event.addEventListener('xzc', a => {
  console.log('后添加xzc' + a);
});

event.emit('xzc', 1, 2, 3); // 先添加xzc12   , 后添加xzc1
event.emit('xzc', 1);       // 先添加xzc1undefined ,后添加xzc1


5.命令模式


命令模式使用场景: 有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者都能消除彼此之间的耦合关系。

假设现在我们需要编写一个按钮界面,界面上有好几个Button按钮,每个按钮有不同的行为。由于现在项目比较复杂,这个活分给了2个人干,A负责绘制按钮,B负责完成点击按钮后的具体行为。这其实可以用命令模式来实现

  • 进入代码编写:首先A绘制这些按钮
    const button1 = document.getElementById('button1');
    const button2 = document.getElementById('button2');
    const button3 = document.getElementById('button3');

    // 由于A不知道这些按钮具体的职责,定义了setCommand 函数,负责往按钮上安装命令。执行的命令操作约定为其中的execute()方法。
    const setCommand = (button, command) => {
      button.onclick = () => {
        command.execute();
      };
    };

  • 接着负责按钮具体行为的B,设计按钮的相关行为
    const refreshButton = {
      execute: function() {
        console.log('button refresh');
      }
    };

    const deleteButton = {
      execute: function() {
        console.log('button delete');
      }
    };

    const updateButton = {
      execute: function() {
        console.log('button update');
      }
    };
  • 最后是整个代码的组合和命令使用
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <button id="button1">button1</button>
    <button id="button2">button2</button>
    <button id="button3">button3</button>
  </body>
  <script>
    //
    const button1 = document.getElementById('button1');
    const button2 = document.getElementById('button2');
    const button3 = document.getElementById('button3');

    // 由于A不知道这些按钮具体的职责,定义了setCommand 函数,负责往按钮上安装命令。执行的命令操作约定为其中的execute()方法。
    const setCommand = (button, command) => {
      button.onclick = () => {
        command.execute();
      };
    };

    const refreshButton = {
      execute: function() {
        console.log('button refresh');
      }
    };

    const deleteButton = {
      execute: function() {
        console.log('button delete');
      }
    };

    const updateButton = {
      execute: function() {
        console.log('button update');
      }
    };
    setCommand(button1, refreshButton); //button refresh
    setCommand(button2, deleteButton); //button delete
    setCommand(button3, updateButton); //button update
  </script>
</html>

以上就是命令模式的一个基本实现,其实命令模式在javascript中是一种隐形的模式,可以用高阶函数非常方便的实现。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <button id="button1">button1</button>
    <button id="button2">button2</button>
    <button id="button3">button3</button>
  </body>
  <script>
    //
    const button1 = document.getElementById('button1');
    const button2 = document.getElementById('button2');
    const button3 = document.getElementById('button3');

    const setCommand = (button, fn) => {
      button.onclick = fn;
    };

    const refreshButton = function() {
      console.log('button refresh');
    };

    const deleteButton = function() {
      console.log('button delete');
    };

    const updateButton = function() {
      console.log('button update');
    };
    setCommand(button1, refreshButton); //button refresh
    setCommand(button2, deleteButton); //button delete
    setCommand(button3, updateButton); //button update
  </script>
</html>


6.组合模式


组合模式思想: 组合模式的主要思想就是用小的对象构建更大的对象,而这些小的子对象本身也许是由更小的孙对象构成的,可以使用树状图表示(如下图)。
它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口来表述,使得客户对单个对象和组合对象使用具有一致性。作为用户,只需要关心树最顶层的组合对象就行。

使用场景:系统对象层次具备整体和部分,呈树形结构,且要求具备统一行为(如树形菜单,操作系统目录结构,公司组织架构等);

举个例子,现在我们需要扫描一个文件夹,而文件夹里既包含了文件,又可以包含其他文件夹,最终可能组成一个树。而我们扫描文件夹,自然不想关心里面有多少文件和子文件夹,组合模式就可以使得我们只需要操作最外层的文件夹进行扫描就行。

  • 进入代码编写:首先定义文件夹Folder类
//创建文件夹类
class Folder {
  constructor(name) {
    this.name = name;
    this.files = []; //文件夹下的文件集合
  }

  //文件夹添加文件
  add(file) {
    this.files.push(file);
  }

  scan() {
    console.log(`开始扫描 ${this.name}文件夹`);
    this.files.forEach(file => file.scan());
  }
}

  • 接着定义文件File类, 根据组合模式思想,需要使得文件类接口和文件夹保持一致性
// 创建文件
class File {
  constructor(name) {
    this.name = name;
  }

  add(file) {
    throw new error('文件不能再添加子文件');
  }

  scan() {
    console.log('开始扫描' + this.name);
  }
}

  • 最后,我们创建一些文件和文件夹,让他们组合成一个树。
const folder1 = new Folder('学习');
const folder2 = new Folder('娱乐');

const file1 = new File('文件1');
const file2 = new File('文件2');
const file3 = new File('文件3');

folder1.add(file1);
folder2.add(file2);

const folder = new Folder('汇总');
folder.add(folder1);
folder.add(folder2);
folder.add(file3);

folder.scan();

//输出结果
// 开始扫描 汇总文件夹
// 开始扫描 学习文件夹
// 开始扫描文件1
// 开始扫描 娱乐文件夹
// 开始扫描文件2
// 开始扫描文件3

通过这个例子,我们可以看到客户是如何同等对待组合对象和叶对象

tips:值得注意的地方

  • 组合模式不是父子关系,是一种聚合关系。组合对象把请求委托给它包含的所有叶对象,他们能够合作的关键是拥有相同的接口
  • 组合模式除了要求组合对象和叶对象拥有相同的接口外,还有一个必要条件就是对一组对象的操作必须具有一致性。比如,公司要给所有员工发过节费1000元,这个场景可以使用组合模式,但是公司如果给今天过生日的员工发蛋糕卷,组合模式就不能起作用了,除非把所有过生日的员工先挑选出来。只有一致的方式对待列表中的每个叶对象,才适合用组合模式
  • 双向映射关系。发过节费的步骤是:公司通知部门,再到各小组,最后到员工手里。这是一个很适合组合模式的场景,但要考虑一种情况是有些员工隶属多个组织架构,那么采用组合模式是不合适的,可能让他们收到多份过节费。
  • 组合对象保存了它下面的子节点的引用,这是组合模式的特点。如果我们删除某个文件的时候,实际上是从这个文件所在的上层文件夹删除的。

7.模板方法模式


模板方法模式定义: 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些待定步骤,模板方法是一种只需要使用即成就可以实现的简单模式。

模板方法模式功能: 在于固定算法骨架,而让具体算法实现可扩展。这在实际应用中非常广泛,尤其是在设计框架级功能的时候非常有用。框架定义好了算法的步骤,在合适的点让开发人员进行扩展,实现具体的算法。模板方法还额外提供了一个好处,就是可以控制子类的扩展,因为在父类中定义好了算法的步骤,只是在某几个固定的点才会调用到被子类实现的方法,因此也就只允许这几个点来扩展功能,这些个可以被子类覆盖及扩展的方法通常被称为钩子方法(例如VUE中生命周期钩子)

变与不变: 程序设计很重要的思考点就是变与不变,也就是分析程序中哪些功能是可变,哪些是不变的,把不变的部分抽象出来,进行公共的实现,把变化的部分分离出来,用接口进行封装。模板方法模式很好的体现了这一点,模板类的实现就是不变的方法和算法的骨架,需要变化的地方,都通过抽象方法,把具体实现延迟到子类,还通过父类的定义约束子类的行为,从而系统有更好的复用性和扩展性。

  • 说这么多,看个经典的咖啡和茶的例子
泡一杯咖啡的步骤 泡一杯茶的步骤
把水煮沸 把水煮沸
用沸水冲泡咖啡 用沸水浸泡茶
把咖啡倒进杯子 把茶水倒进杯子
加糖和牛奶 加枸杞

上面可以发现泡咖啡和茶的不同点:

  • 原料不同:一个咖啡一个是茶,但可以都抽象为饮料
  • 泡的方式不同:一个冲泡,一个浸泡,但可以都抽象为泡
  • 加入的东西不同,一个是糖和牛奶,还有一个是枸杞,但可以抽象为 调料。

经过上面不同点的分析,可以抽象为四个步骤:

  • 把水煮沸
  • 用沸水泡饮料
  • 把饮料倒入被子
  • 加调料

现在开始代码编写:

  • 首先创建一个抽象的父类表示泡一杯饮料的过程。
class Drink {
  //把水煮沸
  boilWater() {
    console.log('把水煮沸');
  }
  //沸水泡饮料
  brew() {}
  //把饮料倒入被子
  pourInCup() {}
  //加调料
  addCondiments() {}

  // 执行整个过程
  init() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    this.addCondiments();
  }
}


  • 创建茶,并执行冲茶过程
//实现茶
class Tea extends Drink {
  //重写沸水泡饮料方法
  brew() {
    console.log('用沸水浸泡茶');
  }
  //重写把饮料倒入被子方法
  pourInCup() {
    console.log('把茶水倒进杯子');
  }
  //重写加调料方法
  addCondiments() {
    console.log('加枸杞');
  }
}
const tea1 = new Tea();
tea1.init();
// 把水煮沸
// 用沸水浸泡茶
// 把茶水倒进杯子
// 加枸杞
  • 创建咖啡,并执行冲咖啡过程
// 实现coffee
class Coffee extends Drink {
  //重写沸水泡饮料方法
  brew() {
    console.log('用沸水冲泡咖啡 ');
  }
  //重写把饮料倒入被子方法
  pourInCup() {
    console.log('把咖啡倒进杯子');
  }
  //重写加调料方法
  addCondiments() {
    console.log('加糖和牛奶');
  }
}

const coffee1 = new Coffee();
coffee1.init();
// 把水煮沸
// 用沸水冲泡咖啡
// 把咖啡倒进杯子
// 加糖和牛奶

上述init即 模板方法,init被称为模板方法的原因是,该方法中封装了子类的算法框架,作为一个算法模板,指导子类以何种顺序去执行哪些方法。

8.中介者模式


中介者模式作用: 主要是解除对象和对象之间的紧耦合关系。增加一个中介对象,所有的相关的对象都通过中介者对象通信,而不是相互作用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者模式使得网状的多对多关系变成了相对简单的一对多关系

来看个实际代码例子

假设我们正在编写一个手机购买的页面,在购买流程中,可以选择手机的颜色以及输入购买数量,同时页面中有两个展示区域,分别向用户展示刚刚选择好的颜色和数量。还有一个按钮动态显示下一步的操作,我们需要查询该颜色手机对应的库存,如果库存数量少于这次的购买数量,按钮将被禁用并且显示库存不足,反之按钮可以点击并且显示放入购物车。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>mediator</title>
  </head>
  <body>
    选择颜色:
    <select id="colorSelect">
      <option value="">请选择</option>
      <option value="red">红色</option>
      <option value="blue">蓝色</option>
    </select>
    <br />

    输入购买数量:<input type="text" id="numInput" /> <br />

    您选择了颜色:
    <div id="colorInfo"></div>
    <br />
    您输入了数量:
    <div id="numInfo"></div>
    <br />

    <button id="btn" disabled="true">请选择手机颜色和购买数量</button>
  </body>
  <script>
    var colorSelect = document.getElementById('colorSelect');
    var numInput = document.getElementById('numInput');
    var colorInfo = document.getElementById('colorInfo');
    var numInfo = document.getElementById('numInfo');
    var btn = document.getElementById('btn');
    var goods = {
      red: 3,
      blue: 4
    };
    // 实现colorSelect.onChange事件 Todo
    // 实现numInput.oninput事件 Todo
  </script>
</html>

考虑一下,触发colorSelect.onChange事件,和numInput.oninput事件会发生什么?

首先,我们要让colorInfo中显示当前选中的颜色,然后获取用户输入的购买数量,对用户输入值进行一些合法的判断等,再根据库存数量来判断btn的状态 如果需求变化。

如果需要新增一个下拉框代表手机内存。由于目前各个节点的对象都是耦合在一起的,改变或者增加任何一个节点,都要通知相关的对象,耦合性较高,代码修改过多。

现在我们引入中介者模式,所有的节点对象都只和中介者对象通信。

  • 首先页面编写
<body>
    选择颜色:
    <select id="colorSelect">
      <option value="">请选择</option>
      <option value="red">红色</option>
      <option value="blue">蓝色</option>
    </select>
    <br />

    选择内存:
    <select id="memSelect">
      <option value="">请选择</option>
      <option value="16G">16G</option>
      <option value="32G">32G</option>
    </select>
    <br />

    输入购买数量:<input type="number" id="numInput" /> <br />

    您选择了颜色:
    <div id="colorInfo"></div>
    <br />
    您选择了内存:
    <div id="memInfo"></div>
    <br />
    您输入了数量:
    <div id="numInfo"></div>
    <br />

    <button id="btn" disabled="true">请选择手机颜色和购买数量</button>
  </body>
  • js代码
<script>
    // 商品 及数量
    var goods = {
      'red|32G': 13,
      'red|16G': 3,
      'blue|32G': 13,
      'blue|16G': 23
    };
    //获取节点
    var colorSelect = document.getElementById('colorSelect');
    var numInput = document.getElementById('numInput');
    var colorInfo = document.getElementById('colorInfo');
    var numInfo = document.getElementById('numInfo');
    var btn = document.getElementById('btn');
    var memSelect = document.getElementById('memSelect');
    var memInfo = document.getElementById('memInfo');

    // 中介者对象
    var mediator = {
      change: obj => {
        var color = colorSelect.value;
        var mem = memSelect.value;
        var num = numInput.value;
        var stock = goods[color + '|' + mem];
        switch (obj) {
          case 'colorSelect':
            colorInfo.innerHTML = color;
            break;
          case 'memSelect':
            memInfo.innerHTML = mem;
            break;
          case 'numInput':
            numInfo.innerHTML = num;
            break;
          default:
            console.log('wait...');
            break;
        }
        if (!color) {
          btn.disabled = true;
          btn.innerHTML = '请选择手机颜色';
          return;
        }
        if (!mem) {
          btn.disabled = true;
          btn.innerHTML = '请选择手机内存';
          return;
        }
        if (num <= 0) {
          btn.disabled = true;
          btn.innerHTML = '请选择正确的数量';
          return;
        }
        if (num > stock) {
          btn.disabled = true;
          btn.innerHTML = '库存不足';
          return;
        }

        btn.disabled = false;
        btn.innerHTML = '立刻购买';
      }
    };
    // 监听事件
    colorSelect.onchange = () => mediator.change('colorSelect');
    memSelect.onchange = () => mediator.change('memSelect');
    numInput.oninput = () => mediator.change('numInput');
  </script>

中介者模式优缺点

  • 优点:中介者模式使得各个对象之间解耦,各个对象只需要关心自身功能的实现,对象之间的交互关系交给了中介者对象来维护
  • 缺点: 会增加一个中介者对象,中介者对象经常是巨大的,难以维护。

9.装饰者模式


装饰者模式定义: 给对象动态的增加职责。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。和继承相比,装饰者模式是一种 即用即付的方式。比如天冷就多穿一件衣服,需要飞行就在头上插一个竹蜻蜓等等。

直接通过实例来说明,例如现在有一个打印参数的函数

var func = function(params) {
  console.log(params); // { a: 'xzc' }
};

func({ a: 'xzc' });

现在不想改变原函数的基础上,给函数func的参数动态的添加属性b,使得输出为{a:'xzc',b:'czcx'}

  • 首先先定义一个aop的装饰函数(关于aop不懂得可以百度一下,这里就不详细讲解了)
//aop的装饰函数
var before = function(fn, beforeFn) {
  return function() {
    // before执行返回的也是一个函数
    beforeFn.apply(this, arguments); // 该函数依次执行beforeFn,和fn,apply(this, arguments)为了防止this劫持
    return fn.apply(this, arguments); // 返回fn,是为了和执行原函数保持一致
  };
};
  • 有了装饰函数后,我们就可以给func函数动态添加属性了
func = before(func, params => {
  params['b'] = '动态加b';
});

func({ a: 'xzc' }); // { a: 'xzc', b: '动态加b' }

10.适配器模式


适配器模式作用: 适配器主要是解决两个软件实体间的接口不兼容的问题。适配器是一个相对简单的模式。有的时候在开发过程中,我们会发现,客户端需要的接口和提供的接口发生不兼容的问题。由于特殊的原因我们无法修改客户端接口。在这种情况下,我们需要适配现有接口和不兼容的类,这就要提到适配器模式。通过适配器,我们可以在不用修改旧代码的情况下也能使用它们,这就是适配器的能力。

  • 首先看一个多态的例子,当我们向googleMaop 和 baiduMap 都发送请求时,都以各自的方式展示地图:
const googleMap = {
  show: () => {
    console.log('谷歌地图');
  }
};
const baiduMap = {
  show: () => {
    console.log('百度地图');
  }
};

const renderMap = map => {
  if (map.show instanceof Function) {
    map.show();
  }
};

renderMap(googleMap); //谷歌地图
renderMap(baiduMap); //百度地图

  • 现在假设baiduMap 提供接口的方法不叫show,而是display
const baiduMap = {
  display: () => {
    console.log('百度地图');
  }
};
  • 我们通过适配器模式,来解决问题
const googleMap = {
  show: () => {
    console.log('谷歌地图');
  }
};
const baiduMap = {
  display: () => {
    console.log('百度地图');
  }
};

const renderMap = map => {
  if (map.show instanceof Function) {
    map.show();
  }
};

const baiduAdapter = {
  show: () => {
    return baiduMap.display();
  }
};

renderMap(googleMap); //谷歌地图
renderMap(baiduAdapter); //百度地图

如果现在的接口已经能够正常工作,那我们不会用到适配器模式,适配器模式是一种亡羊补牢的模式。

11.状态模式


状态模式:状态模式的关键是区分事务内部的状态,事物内部的状态的改变往往会带来事物的行为改变。

  • 想象下这样一个情景。现在有一个开关,初始是关闭状态,按一次是弱灯状态,再按一次是强灯状态,再按一次就关闭了。现在用代码描述这个场景
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <button id="btn">关</button>
  </body>
  <script>
    var btn = document.getElementById('btn');
    var state = 'off';
    var clickBtn = function() {
      if (this.state === 'strong') {
        this.state = 'off';
        btn.innerHTML = '关';
      } else if (this.state === 'off') {
        this.state = 'light';
        btn.innerHTML = '弱灯';
      } else if (this.state === 'light') {
        this.state = 'strong';
        btn.innerHTML = '强灯';
      }
    };
    btn.onclick = function() {
      this.clickBtn();
    }.bind(this);
  
  </script>
</html>

这是很容易想到的一个写法,但代码中充斥了if else 语句,现在逻辑就一行代码,如果逻辑更加复杂的话,或者再来几个状态,那么无法预计这个方法膨胀到什么地步。

下面我们使用状态模式修改下代码

<script>
    var btn = document.getElementById('btn');
    // var state = 'off';
    // var clickBtn = function() {
    //   if (this.state === 'strong') {
    //     this.state = 'off';
    //     btn.innerHTML = '关';
    //   } else if (this.state === 'off') {
    //     this.state = 'light';
    //     btn.innerHTML = '弱灯';
    //   } else if (this.state === 'light') {
    //     this.state = 'strong';
    //     btn.innerHTML = '强灯';
    //   }
    // };
    // btn.onclick = function() {
    //   this.clickBtn();
    // }.bind(this);

    const fsm = {
      off: {
        press: function() {
          currentState = fsm.light;
          btn.innerHTML = '弱灯';
        }
      },
      light: {
        press: function() {
          currentState = fsm.strong;
          btn.innerHTML = '强灯';
        }
      },
      strong: {
        press: function() {
          currentState = fsm.off;
          btn.innerHTML = '关';
        }
      }
    };
    let currentState = fsm.off;

    btn.onclick = function() {
      currentState();
    }.bind(this);
  </script>

状态模式优点

  • 状态及状态的切换一目了然
  • 避免了过多的if else
  • 用对象代替字符串记录当前状态, 状态易维护

状态模式缺点

  • 需要编写大量的状态类对象

12.责任链模式


责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接受者的耦合关系,将这些对象形成一条链,并沿着这条链,传递请求,直到有一个对象处理为止。

责任链模式(白话解释下):当一个对象A要办个事儿,就向一个处理对象B发起请求,如果B不处理,可以把请求转给C,如果C不处理,又可以把请求转给D。一直到有一个对象愿意处理这个请求为止。B -> C -> D 这就是一条职责链。如果请求一直没人处理,那么这事儿就黄了,搞不定。A在一开始是不知道自己会被谁处理的。

  • 想象下这样一个情景。有个网站,vvip可以观看所有视频,vip可以大部分视频,普通用户common不可以看视频。
const video = userType => {
  if (userType === 'vvip') {
    //todo
    console.log('可以观看全部视频');
  } else if (userType === 'vip') {
    //todo
    console.log('可以观看部分视频');
  } else if (userType === 'common') {
    //todo
    console.log('不可以观看视频');
  }
};

video('vvip'); //可以观看全部视频

这是很容易想到的一个写法

下面我们使用责任链模式修改下代码

const vvipVideo = userType => {
  if (userType === 'vvip') {
    //todo
    console.log('可以观看全部视频');
  } else {
    return true; //不知道下一个节点是什么,反正把请求向后传递
  }
};
const vipVideo = userType => {
  if (userType === 'vip') {
    //todo
    console.log('可以观看部分视频');
  } else {
    return true; //不知道下一个节点是什么,反正把请求向后传递
  }
};
const commonVideo = userType => {
  if (userType === 'common') {
    //todo
    console.log('不可以观看视频');
  } 
};

class Chain {
  constructor(fn) {
    this.fn = fn;     //当前函数
    this.next = null;  //下一个节点
  }
  
  // 指定下一个节点
  setNext(next) {
    return (this.next = next);
  }
  
  execute() {
  // 执行本次函数
    const ret = this.fn.apply(this, arguments);
    
    // 如果本地函数执行完毕后,显示有下一个节点,那么执行下一个节点函数
    return ret ? this.next && this.next.execute.apply(this.next, arguments) : ret;
  }
}

const chainVvip = new Chain(vvipVideo);
const chainVip = new Chain(vipVideo);
const chainCommon = new Chain(commonVideo);

chainVvip.setNext(chainVip).setNext(chainCommon);

chainVvip.execute('vip');
chainVvip.execute('common');
chainVvip.execute('vvip');

由上例可见,我们只要将请求传给职责链中的第一环chainVvip就可以了,如果它处理不了,就会将请求传给下一环节。

责任链模式应用
学过BOM知识的童鞋肯定有所了解,浏览器的事件冒泡,事件委托,事件代理的过程。实际可以用责任链来解释:一个事件在某个节点上被触发,然后向根节点传递,直到被节点捕获。 JavaScript的原型链也有责任链的影子: 在子类上调用一个方法,如果自身找不到,就沿着原型链一级级向上查找。

(本文基于Javascript设计模式与开发实践 书籍思想)