设计模式解析与实战

# 设计模式解析与实战

# 前端设计模式

# 基础概念

# 面向对象

面向对象有继承,封装,多态的特性,面向对象优势很容易看出,

  1. 容易维护
  2. 容易扩展
  3. 容易复用
  • 面向过程

其实是面向对象的底层,只是我们把这个过程写在了对象的类上了

# es6继承

class A{
    Afun(){

    }
}
class B extends A{

}
let b = new B()
b.Afun() // b 可以继承A的方法

# 原型链继承

function A(){}
A.prototype.Afun = function() {}
function B(){}
B.prototype = new A()
let b = new B()
b.Afun() // b通过原型链 prototype 可以调用到A的方法
// 问题,当前并不能执行A的方法只是继承到来A到方法(而且A方法居然在之前就使用过一次)

// 构造函数
function A(a){this.a = a}
A.prototype.Afun = function() {console.log(123)}
function B(){A.call(this,1)}
let b = new B()
b.a // 可以获取到当前
// 问题,当前并不能继承A的方法,只能使用A方法

// 混合使用
function A(a){
    this.a = a
}
A.prototype.Afun = function() {
    console.log('Afun')
}
function B(){
    A.call(this,1)
}
B.prototype             = A.prototype
B.prototype.constructor = A
let b = new B()
console.log(b.a)
b.Afun()
// 这样就可以使用当前A的方法,不懂的小伙伴可以自己码一下试试

# 设计原则

  1. 单一职责原则(类功能要单一,不能什么功能都往类里面写)
  2. 开放封闭原则(对扩展开放,对修改封闭,可以进行对功能对扩展,但是减少对功能对修改)
  3. 里式替换原则(前端不咋用)
  4. 依赖倒置原则(前端不咋用)
  5. 接口分离原则(前端不咋用,都没有接口)

# 常见模式

# 工厂模式

工厂模式(Factory Pattern):将对象的创建和使用分离,由工厂类负责创建对象并返回。在前端开发中,可以使用工厂模式来动态创建组件。

前端中的工厂模式是一种创建对象的设计模式,它可以让我们封装创建对象的细节,我们使用工厂方法而不是直接调用 new 关键字来创建对象,使得代码更加清晰、简洁和易于维护。在前端开发中,工厂模式通常用于创建多个相似但稍有不同的对象,比如创建一系列具有相同样式和行为的按钮或者表单。

例如,我们可以创建一个ButtonFactory函数,它接受一个type参数,用于指定按钮的类型,然后根据type参数创建不同类型的按钮对象。示例代码如下:

function ButtonFactory(type) {
  switch (type) {
    case 'primary':
      return new PrimaryButton();
    case 'secondary':
      return new SecondaryButton();
    case 'link':
      return new LinkButton();
    default:
      throw new Error('Unknown button type: ' + type);
  }
}

function PrimaryButton() {
  this.type = 'primary';
  this.text = 'Click me!';
  this.onClick = function() {
    console.log('Primary button clicked!');
  };
}

function SecondaryButton() {
  this.type = 'secondary';
  this.text = 'Click me too!';
  this.onClick = function() {
    console.log('Secondary button clicked!');
  };
}

function LinkButton() {
  this.type = 'link';
  this.text = 'Click me as well!';
  this.onClick = function() {
    console.log('Link button clicked!');
  };
}

在上面的示例中,ButtonFactory函数接受一个type参数,根据这个参数来创建不同类型的按钮对象。例如,如果type为'primary',则返回一个PrimaryButton对象,该对象具有type、text和onClick属性,表示一个主要按钮。其他类型的按钮也类似。

使用工厂模式可以让我们将对象创建的过程与具体的业务逻辑分离开来,从而提高代码的可重用性和可维护性。

# 单例模式

单例模式(Singleton Pattern):保证一个类只有一个实例,并提供一个访问它的全局访问点。在前端开发中,可以使用单例模式来管理全局状态和资源。

在JavaScript中,每个构造函数都可以用于创建单例对象,例如:

function Singleton() {

  if (typeof Singleton.instance === "object") {
    return Singleton.instance;
  }

  this.property1 = "value1";
  this.property2 = "value2";
  Singleton.instance = this;
}

const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2);


// ts
class Singleton {
    static instance: Singleton | null = null

    prop1 = 'value1';
    prop2 = 'value2';

    constructor() {
        if (Singleton.instance) {
            return Singleton.instance
        }
        Singleton.instance = this
    }

}

前端用于:状态管理,全局toast插件等..

# 发布-订阅模式

发布-订阅模式(Publish-Subscribe Pattern):也叫消息队列模式,它是一种将发布者和订阅者解耦的设计模式。在前端开发中,可以使用发布-订阅模式来实现组件之间的通信。

JavaScript中的发布/订阅模式(Pub/Sub)是一种常用的设计模式。它允许在应用程序中定义对象之间的一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会被通知和更新

在发布/订阅模式中,有两种类型的对象:发布者和订阅者。发布者是事件的发出者,它通常维护一个事件列表,并且可以向列表中添加或删除事件。当某个事件发生时,它会将这个事件通知给所有订阅者。订阅者则是事件的接收者,它们订阅感兴趣的事件,并且在事件发生时接收通知。

发布订阅模式可以帮助我们实现松耦合的设计,让对象之间的依赖关系变得更加灵活。它在前端开发中的应用非常广泛,例如 Vue.js 中的事件总线、Redux 中的 store 等。


// 发布者
var publisher = {

  events: {},

//   添加事件
  addEvent: function(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

// 移除事件
  removeEvent: function(event, callback) {
    if (this.events[event]) {
      for (var i = 0; i < this.events[event].length; i++) {
        if (this.events[event][i] === callback) {
          this.events[event].splice(i, 1);
          break;
        }
      }
    }
  },

// 发布事件
  publishEvent: function(event, data) {
    if (this.events[event]) {
        // 遍历通知订阅者,执行回调
      for (var i = 0; i < this.events[event].length; i++) {
        this.events[event][i](data);
      }
    }
  }
};

// 订阅者
var subscriber = {

  handleEvent: function(data) {
    console.log(data);
  }
};

// 发布者添加事件,并添加回调函数
publisher.addEvent('event1', subscriber.handleEvent);

// 发布者发布事件
publisher.publishEvent('event1', 'Hello, world!');

// 发布者移除事件
publisher.removeEvent('event1', subscriber.handleEvent);

在这个例子中,发布者对象维护了一个事件列表(events),并且提供了添加、删除和发布事件的方法。订阅者对象则提供了一个处理事件的回调函数(handleEvent),它可以被添加到发布者对象的事件列表中。当发布者发布一个事件时,所有订阅了这个事件的订阅者都会收到通知,并执行相应的处理函数。

# 观察者模式

观察者模式(Observer Pattern):当对象间存在一对多的关系时,使用观察者模式。当被观察的对象发生变化时,其所有的观察者都会收到通知并进行相应的操作。在JavaScript中,可以使用回调函数或事件监听来实现观察者模式。

前端中用的较多(Rx.js)

在前端开发中,观察者模式常被用来实现组件间的数据传递和事件处理。比如,当一个组件的状态发生改变时,可以通过观察者模式来通知其他组件更新自身的状态或视图。

// 定义被观察者
class Subject {
  constructor() {
    // 观察者队列
    this.observers = []
  }

// 添加观察者
  addObserver(observer) {
    this.observers.push(observer)
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer)
  }

// 通知观察者
  notify(data) {
    this.observers.forEach(obs => obs.update(data))
  }
}

// 定义观察者
class Observer {
    // 更新事件
  update(data) {
    console.log(`Received data: ${data}`)
  }
}

const subject = new Subject()
const observer1 = new Observer()
const observer2 = new Observer()

subject.addObserver(observer1)
subject.addObserver(observer2)

subject.notify('Hello, world!')

subject.removeObserver(observer1)

subject.notify('Goodbye, world!')

在上面的示例中,我们定义了一个 Subject 类和一个 Observer 类。Subject 类有三个方法,addObserver 用于添加观察者,removeObserver 用于移除观察者,notify 用于通知所有观察者。

Observer 类只有一个方法 update,用于接收主题传递的数据。我们创建了两个 Observer 实例并将它们添加到了 Subject 实例中,然后调用了 notify 方法来通知它们更新数据。

# 装饰者模式

装饰者模式(Decorator Pattern):动态地给一个对象添加额外的职责。在前端开发中,可以使用装饰者模式来动态修改组件的行为和样式。

前端中用的较多(Nest.js, Angular)


function Foo() {}

Foo.prototype.foo = function() {
  console.log('foo');
}

function barDecorator(clazz) {
  clazz.prototype.bar = function() {
    console.log('bar');
  }
}

barDecorator(Foo);

const obj = new Foo();
obj.foo();
obj.bar();

在上面的示例中,我们首先定义了一个原始对象 Foo,它是一个构造函数,用于创建一个对象。然后,我们在原型上定义了一个方法 foo。接着,我们定义了一个装饰函数 barDecorator,它接收一个构造函数作为参数,用于在原型上添加一个新的方法 bar。最后,我们使用 barDecorator 函数来扩展原始对象的原型,然后创建一个新的对象 obj,并使用扩展后的方法 foo 和 bar。

# 依赖注入模式

依赖注入模式(Dependency Injection Pattern):允许我们通过将对象的依赖关系从代码中分离出来,从而使代码更加模块化和可重用。

在 Angular Nest.js 框架中使用

class UserService {
  constructor(apiService) {
    this.apiService = apiService;
  }

  getUser(id) {
    return this.apiService.get(`/users/${id}`);
  }
}

class ApiService {
  constructor(httpService) {
    this.httpService = httpService;
  }

  get(url) {
    return this.httpService.get(url);
  }
}

class HttpService {
  get(url) {

  }
}

const httpService = new HttpService();
const apiService = new ApiService(httpService);
const userService = new UserService(apiService);

userService.getUser(1);

在上面的代码中, UserService、 ApiService、 HttpService三个类之间都存在依赖关系。使用依赖注入模式,可以将这些依赖关系从内部移到外部,从而实现对象之间的解耦。在实例化 UserService对象时,将依赖的 ApiService对象作为参数传入构造函数;在实例化 ApiService对象时,将依赖的 HttpService对象作为参数传入构造函数。这样就实现了依赖注入。

# 设计模式常见面试

# Vue的响应式原理

在改变数据的时候,视图会随之更新,意味着我们只需要管理数据。vue是利用Object.defineProperty劫持对象属性的setter 与getter方法,利用发布订阅的设计模式,数据的读操作都会触发 getter 函数,写操作都会触发 setter 函数。

  • Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy ?
  1. Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  2. Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
  3. 在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者 给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到 这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通 过重写函数的方式解决了这个问题。

使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性 的问题,因为 Proxy 是 ES6 的语法。

# Computed 和 Watch 的区别

  • Computed:

它支持缓存,只有依赖的数据发生了变化,才会重新计算

不支持异步,当Computed中有异步操作时,无法监听数据的变化

computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是 基于data声明过,或者父组件传递过来的props中的数据进行计算的。

如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed

在computed中,属性有一个get方法和一个set方法,当数据发生变化时, 会调用set方法。

  • Watch:

它不支持缓存,数据变化时,它就会触发相应的操作

支持异步监听

监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值

当一个属性发生变化时,就需要执行相应的操作

监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时, 会触发其他操作,函数有两个的参数:

immediate:组件加载立即触发回调函数

deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的 对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。

  1. computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依 赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的 值。
  2. watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调, 每当监听的数据变化时都会执行回调进行后续操作。

# 备注

  • 面试题:手动实现一个单例模式、发布订阅模式?js/ts实现?

  • 发布订阅模式跟观察者模式的区别?

# 参考

上次更新: 11/26/2023, 7:38:38 PM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>