前言

设计模式是对软件设计中普遍存在的各种问题所提出的解决方案。

可以简单理解为程序开发的一些套路。

当我们遇到合适的场景时,可能会条件反射一样想到符合这种场景的设计模式。 比如,有个组件不能满足现有需求,需要给它加上新功能。组件内业务相对独立,我们并不想修改这个组件。

这时候,我们就可以使用装饰器模式。

构造器模式

有下面两个对象:

const jack = {
  name: 'jack',
  age: 18
};
const jim = {
  name: 'jim',
  age: 17
};

我们把这两个对象的属性抽象出来,就是构造器模式。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const jack = new Person('jack', 18);
const jim = new Person('jim', 17);

原型模式

现在我们想在 Person 里加一个 say 方法, 让 jack 和 jim 都可以使用这个 say 方法:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.say = function () {
    console.log('说话');
  };
}
const jack = new Person('jack', 18);
const jim = new Person('jim', 17);
console.log(jack.say === jim.say); // false

直接把方法加到构造器里, 不同的实例会各自存储一份,造成内存浪费。如何解决这个问题呢?

我们可以把 say 方法加到 Person 的原型上:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function () {
  console.log('说话');
};
const jack = new Person('jack', 18);
const jim = new Person('jim', 17);
console.log(jack.say === jim.say); // true

这样就解决了内存浪费的问题。也就是原型模式。

简单工厂模式

只需要一个参数,就可以获取到我们所需要的对象,而无需知道创建细节。

class Dress {}
class Shirt {}

class ClothFactory {
  static create(type) {
    switch (type) {
      case 'dress':
        return new Dress();
      case 'shirt':
        return new Shirt();
      default:
        break;
    }
  }
}
const dress = ClothFactory.create('dress');
const shirt = ClothFactory.create('shirt');
console.log(dress); // dress实例
console.log(shirt); // shirt实例

可以看到,我们通过不用关心怎么创建 Dress 和 Shirt 只需要传入参数,ClothFactory 会帮助我们创建。

抽象工厂模式

抽象工厂模式就是通过类的抽象使得业务适用于一个产品类簇的创建,而不负责某一个类产品的实例。

class Cloth {
  constructor() {
    if (new.target === Cloth) {
      throw new Error('Cloth是抽象类, 不能实例化');
    }
  }
  consoleClothName() {
    throw new Error('consoleClothName要被重写');
  }
}
class Dress extends Cloth {
  constructor() {
    super();
    this.name = '裙子';
  }
  consoleClothName() {
    console.log('裙子');
  }
}
class Shirt extends Cloth {
  constructor() {
    super();
    this.name = '衬衫';
  }
  consoleClothName() {
    console.log('衬衫');
  }
}

class ClothFactory {
  static create(type) {
    switch (type) {
      case 'dress':
        return Dress;
      case 'shirt':
        return Shirt;
      default:
        break;
    }
  }
}

const DressClass = ClothFactory.create('dress');
new DressClass().consoleClothName(); // 裙子
new Cloth(); // Uncaught Error: Cloth是抽象类, 不能实例化

单例模式

单例模式保证类只能被实例化一次。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static getInstance(name, age) {
    // 类函数中的 this 指向类
    if (!this.instance) {
      this.instance = new Person(name, age);
    }
    return this.instance;
  }
}
const jack = Person.getInstance('Jack', 18);
const jim = Person.getInstance('Jim', 18);
console.log(jack === jim); // true

第二次与第一次返回的是同一个实例,保证了 Person 类只被实例化了一次。

也可以单例的处理放到构造器中,保证我们每次 new 出来的实例都是同一个。

class Person {
  constructor(name, age) {
    if (!Person.instance) {
      this.name = name;
      this.age = age;
      Person.instance = this;
    }
    return Person.instance;
  }
}
const jack = new Person('Jack', 18);
const jim = new Person('Jim', 18);
console.log(jack === jim);

可以看到,两次 new 实例返回的是同一个实例对象。

使用闭包也同样可以实现单例模式:

var singleton = (function (name, age) {
  function Person(name, age) {
    this.name = name;
    this.age = age;
  }

  var instance = null;
  return function () {
    if (!instance) {
      instance = new Person(name, age);
    }
    return instance;
  };
})();

console.log(singleton('jack', 18) === singleton('jim', 16)); // true

装饰器模式

装饰器模式就是在不改变原有对象的基础上,对其进行包装扩展。该模式应用非常广泛,比如:节流和防抖函数、React 的高阶组件都是装饰器模式的实际应用。

class Person {
  sayChinese() {
    console.log('我会说中文');
  }
}

class PersonDecorator {
  constructor(person) {
    this.person = person;
  }
  sayChinese() {
    this.person.sayChinese();
  }
  sayEnglish() {
    console.log('我会说英文');
  }
}

const jack = new Person();
jack.sayChinese();

const newJack = new PersonDecorator(jack);
newJack.sayChinese();
newJack.sayEnglish(); // 我会说英文

我们通过包装扩展,实现了 newJack 能够 sayEnglish 的效果。

适配器模式

适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。不影响现有实现方式,兼容调用旧接口的代码

class PersonA {
  say() {
    console.log('说话');
  }
}

class PersonB {
  speak() {
    console.log('说话');
  }
}

new PersonA().say();
new PersonB().say(); // Uncaught TypeError: (intermediate value).say is not a function

PersonB 通过 speak 才能说话,可是我们现在想要让 PersonB 与 PersonA 保持一致,通过 say 来说话。我们可以写一个适配器类:

class PersonBAdapter {
  constructor(personB) {
    this.personB = personB;
  }
  say() {
    this.personB.speak();
  }
}

new PersonBAdapter().say(); // 说话

代理模式

出于某种考虑,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的。这样的模式就是代理模式。 比如,老板喜欢喝咖啡,但是买咖啡这件事情交给秘书去做,秘书就是老板的代理。

class Coffee {
  drink() {
    console.log('咖啡好喝');
  }
}
class Secretary {
  buyCoffee() {
    return new Coffee();
  }
}
class Boss {
  buyCoffee() {
    return new Secretary().buyCoffee();
  }
}

const myBoss = new Boss();
myBoss.buyCoffee().drink(); // 咖啡好喝

代码中可以看出,老板没有实际去买咖啡(new Coffee()这个操作),而是交给了秘书去做。

有 4 中常见的代理应用场景:事件代理、保护代理、虚拟代理和缓存代理。

事件代理

有这样一个场景:一个页面有 100 个按钮,每个按钮点击会弹出 hello。如果给每个按钮都添加点击事件,则需要 100 个点击事件,极其消耗性能。

不妨利用事件冒泡的原理,把事件加到它们的公共父级上,触发执行效果。这样一方面能够减少事件数量,提高性能;另一方面对于新添加的元素,依然可以触发该事件

保护代理

顾名思义,保护目标对象,替他过滤掉不必要的操作。比如下面这个例子,秘书替老板过滤掉不必要的员工建议。

class Employee {
  advice(secretary, content) {
    secretary.handleAdvice(content);
  }
}
class Secretary {
  constructor(boss) {
    this.boss = boss;
  }
  handleAdvice(content) {
    if (content === '好建议') {
      this.boss.getAdvice(content);
    } else {
      console.log('老板只接受好建议');
    }
  }
}
class Boss {
  getAdvice(content) {
    console.log(content);
  }
}

const boss = new Boss();
const secretary = new Secretary(boss);
new Employee().advice(secretary, '好建议'); // 好建议
new Employee().advice(secretary, '坏建议'); // 老板只接受好建议

虚拟代理

虚拟代理是把一些开销很大的对象,延迟到真正需要它的时候才去创建执行。

缓存代理

缓存代理就是将消耗性能的工作加上缓存,同样的工作下次直接使用缓存即可。比如下面这个例子,我们通过代理对累加做缓存:

function addAll() {
  console.log('计算');
  let sum = 0;
  for (let i = 0, len = arguments.length; i < len; i++) {
    sum += arguments[i];
  }
  return sum;
}

const addProxy = (function () {
  let cache = {};
  return function () {
    const type = Array.prototype.join.call(arguments);
    if (!cache[type]) {
      cache[type] = addAll.apply(this, arguments);
    }
    return cache[type];
  };
})();

console.log(addProxy(3, 4, 5)); // 12
console.log(addProxy(3, 4, 5)); // 12
console.log(addProxy(3, 4, 5)); // 12

最终,三次结果都返回 12,但是只计算了一次。

策略模式

定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。

策略模式是比较常用的模式。拿年终奖举例,S 绩效 4 倍月薪,A 绩效 3 倍月薪,B 绩效 2 倍月薪。正常一个计算奖金的函数如下

function bonus(performance, salary) {
  if (performance === 'S') {
    return 4 * salary;
  }
  if (performance === 'A') {
    return 3 * salary;
  }
  if (performance === 'B') {
    return 2 * salary;
  }
}

这个函数违背了单一功能原则,一个函数里处理了 3 份逻辑。我们把这三份逻辑单独抽离出来。

function performanceS(salary) {
  return 4 * salary;
}
function performanceA(salary) {
  return 3 * salary;
}
function performanceB(salary) {
  return 2 * salary;
}

function bonus(performance, salary) {
  if (performance === 'S') {
    return performanceS(salary);
  }
  if (performance === 'A') {
    return performanceA(salary);
  }
  if (performance === 'B') {
    return performanceB(salary);
  }
}

代码看上去好像更复杂了,但是代码的逻辑更清晰了。假如每个计算逻辑都比较复杂的话,无疑这样处理更有利于理解。 平时工作中,针对该问题,可能更多是下面这种处理方式:

const performanceStrategies = {
  S: 4,
  A: 3,
  B: 2
};

function bonus(performance, salary) {
  return performanceStrategies[performance] * salary;
}

观察者模式

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

class Publisher {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    this.observers.forEach((item, index) => {
      if (item === observer) {
        this.observers.splice(index, 1);
      }
    });
  }

  notify() {
    this.observers.forEach((item) => {
      item.update();
    });
  }
}

class Observer1 {
  update() {
    console.log('observer1接收到了通知');
  }
}
class Observer2 {
  update() {
    console.log('observer2接收到了通知');
  }
}

const publisher = new Publisher();
const observer1 = new Observer1();
const observer2 = new Observer2();
publisher.addObserver(observer1);
publisher.addObserver(observer2);
publisher.notify(); // observer1接收到了通知 observer2接收到了通知

发布订阅模式

发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在

class PubSub {
  constructor() {
    this.messageObj = {};
    this.listenerObj = {};
  }

  addPublish(type, content) {
    if (!this.messageObj[type]) {
      this.messageObj[type] = [];
    }
    this.messageObj[type].push(content);
  }

  addSubscribe(type, callback) {
    if (!this.listenerObj[type]) {
      this.listenerObj[type] = [];
    }
    this.listenerObj[type].push(callback);
  }

  notify(type) {
    const messageList = this.messageObj[type];
    (this.listenerObj[type] || []).forEach((callback) => callback(messageList));
  }
}

class Publisher {
  constructor(name, pubsub) {
    this.name = name;
    this.pubsub = pubsub;
  }
  publish(type, content) {
    this.pubsub.addPublish(type, content);
  }
}

class Subscriber {
  constructor(name, pubsub) {
    this.name = name;
    this.pubsub = pubsub;
  }
  subscribe(type, callback) {
    this.pubsub.addSubscribe(type, callback);
  }
}

const pubsub = new PubSub();

const publishA = new Publisher('publishA', pubsub);
publishA.publish('A', 'this is a');
publishA.publish('A', 'this is another a');
publishA.publish('B', 'this is b');
const publishB = new Publisher('publishB', pubsub);

const subscribeA = new Subscriber('subscribeA', pubsub);
subscribeA.subscribe('A', (res) => console.log(res));
subscribeA.subscribe('B', (res) => console.log(res));

pubsub.notify('A'); // ['this is a', 'this is another a']
pubsub.notify('B'); // ['this is b']

代码中我们创建了两个发布者, publishA 和 publishB : publishA 发布了两个 A 类型信息,publishB 发布了一个 B 类型信息

一个订阅者, subscribeA : subscribeA 订阅了 A 类型和 B 类型的信息

享元模式

享元模式是一种用于性能优化的模式,享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

一个简单的例子:假如有个商店,电里有 50 种衣服,我们需要让塑料模特穿上衣服拍照。正常情况下,需要 50 个塑料模特。

class Model {
  constructor(cloth) {
    this.cloth = cloth;
  }
  takePhoto() {
    console.log(`cloth:${this.cloth}`);
  }
}

for (let i = 0; i < 50; i++) {
  const model = new Model(i);
  model.takePhoto();
}

但是仔细考虑下,我们并不需要这么多塑料模特,塑料模特可以反复使用,一个就够了。

const model = new Model();
for (let i = 0; i < 50; i++) {
  model.cloth = i;
  model.takePhoto();
}

通过复用,只需要一个模型实例就完成了同样的功能。 实际开发中的情况可能更复杂一些,但是核心是一样的。通过共享来优化性能。常见的应用有线程池、对象池等。