Inventory of commonly used design patterns in JavaScript

foreword

Design patterns are solutions to various problems that commonly exist in software design.

It can be simply understood as some routines of program development.

When we encounter a suitable scene, we may think of a design pattern that matches this scene like a conditioned reflex. For example, a component cannot meet the existing requirements, and new functions need to be added to it. The business within the component is relatively independent, and we don't want to modify this component.

At this time, we can use the decorator pattern.

constructor pattern

There are the following two objects:

const jack = {
    
    
  name'jack',
  age18
};
const jim = {
  name'jim',
  age17
};

We abstract the properties of these two objects, which is the constructor pattern.

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

prototype pattern

Now we want to add a say method in Person, so that both jack and jim can use this say method:

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

Add the method directly to the constructor, and each instance will store a copy, resulting in a waste of memory. How to solve this problem?

We can add the say method to the Person prototype:

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

This solves the problem of memory waste. That is the prototype mode.

Simple Factory Pattern

Only one parameter is needed to get the object we need without knowing the details of creation.

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实例

It can be seen that we don't care about how to create Dress and Shirt and only need to pass in parameters, and ClothFactory will help us create them.

abstract factory pattern

The abstract factory pattern is to make the business applicable to the creation of a product class cluster through the abstraction of the class, and is not responsible for the instance of a certain class product.

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是抽象类, 不能实例化

singleton pattern

The singleton pattern guarantees that a class can only be instantiated once.

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

The same instance is returned the second time and the first time, ensuring that the Person class is only instantiated once.

You can also put the singleton processing in the constructor to ensure that the instance we create every time is the same.

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);

It can be seen that the two new instances return the same instance object.

The singleton pattern can also be implemented using closures:

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

decorator pattern

The decorator pattern is to wrap and extend the original object without changing it. This pattern is widely used, for example: throttling and anti-shake functions, React's high-level components are practical applications of the decorator pattern.

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(); // 我会说英文

We achieved the effect that newJack can say English through packaging extension.

adapter pattern

The adapter pattern can help us solve incompatibility problems by transforming the interface of a class into another interface that the client expects. Does not affect the existing implementation, compatible with the code calling the old interface

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(thisarguments);
    }
    return cache[type];
  };
})();

console.log(addProxy(345)); // 12
console.log(addProxy(345)); // 12
console.log(addProxy(345)); // 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();
}

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

参考文档:

https://juejin.cn/post/7072175210874535967#heading-69

本文由 mdnice 多平台发布

Guess you like

Origin blog.csdn.net/ppppppppppsd/article/details/129282590