常见前端开发设计模式 | 青训营

113 阅读7分钟

前端开发设计模式


前端设计模式分为三大类:结构型模式、创建型模式、行为型模式。

常见的结构型模式:

1.工厂模式

工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象。

  • 优点

    • 封装对象的创建过程,提高了代码的可维护性和可扩展性。
    • 可以根据需要返回不同类型的对象,增加了灵活性。
    • 降低了代码中类之间的耦合。
  • 缺点

    • 引入了额外的工厂类,可能会增加代码复杂性。
    • 当工厂方法数量庞大时,可能会导致类爆炸。

案例:

 // 汽车构造函数
 function SuzukiCar(color) {
   this.color = color;
   this.brand = 'Suzuki';
 }
 ​
 /**
  * 汽车工厂
  */
 function CarFactory() {
   this.create = (brand, color)=> {
         return new SuzukiCar(color);
   }
 }
 ​
 //使用工厂
 const carFactory = new CarFactory();
 const cars = [];
 cars.push(carFactory.create(BRANDS.suzuki, 'brown'));
 ​
 function sayHello() {
   console.log(`Hello, I am a ${this.color} ${this.brand} car`);
 }
 for (const car of cars) {
   sayHello.call(car);
 }
使用工厂模式后,不再需要重复引入一个构造函数,只需要引入工厂对象就可以方便地创建各类对象。
2.单例模式

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

  • 优点

    • 确保一个类只有一个实例,节省了系统资源。
    • 提供了全局访问点,方便在整个应用程序中共享数据和状态。
  • 缺点

    • 可能会引入全局状态,导致调试和测试困难。
    • 过度使用单例模式可能会导致代码紧耦合,降低了可维护性。
  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

案例:

 class Person{
   constructor(){}
 }
 ​
 let p1 = new Person();
 let p2 = new Person();
 ​
 console.log(p1===p2) //false
定义一个Person类,通过这个类创建两个示例,可以看到两个实例不相等,即通过同一个类得到的示例不是同一个。
3.原型模式

原型模式实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

  • 优点

    • 允许动态创建对象,而无需事先知道其类型。
    • 减少了对象的创建成本,特别是当对象初始化过程复杂时。
  • 缺点

    • 需要正确实现对象的克隆方法。
    • 可能会导致对象状态的混淆,需要小心处理。

案例:

 let person = {
   name:'hello',
   age:24
 }
 ​
 let anotherPerson = Object.create(person);
 console.log(anotherPerson.__proto__)  //{name: "hello", age: 24}
 ​
 anotherPerson.name = 'world';  //可以修改属性
 anotherPerson.job = 'teacher';
可以通过Object.create()使用现有的对象来提供新创建的对象的_proto_。另外也可以使用原型继承来实现原型模式。
 function F(){}
 ​
 F.prototype.g = function(){}
  
 //G类继承F类
  
 function G(){
   F.call(this);
 }
  
 //原型继承
 function Fn(){};
 Fn.prototype = F.prototype;
 G.prototype = new Fn();
  
 G.prototype.constructor = G;

常见的创建型模式:

1.装饰器模式

装饰器模式通过将对象包装在装饰器类中,以便动态地修改其行为。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

  • 优点

    • 允许在运行时为对象添加新功能,而无需修改其代码。
    • 支持递归组合,可以创建复杂的对象结构。
  • 缺点

    • 可能会引入大量小对象,增加了内存消耗。
    • 如果不谨慎使用,可能导致过度复杂化的设计。

案例:

 class Circle {
     draw() {
         console.log('画一个圆形');
     }
 }
 ​
 class Decorator {
     constructor(circle) {
         this.circle = circle;
     }
     draw() {
         this.circle.draw();
         this.setRedBorder(circle);
     }
     setRedBorder(circle) {
         console.log('设置红色边框')
     }
 }
 ​
 // 测试
 let circle = new Circle();
 ​
 let client = new Decorator(circle);
 client.draw();

输出结果:

 画一个圆形
 设置红色边框
2.适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。

  • 优点

    • 允许将不同接口的对象协同工作。
    • 可以重用现有的类,无需修改其代码。
  • 缺点

    • 增加了代码复杂性,因为需要引入适配器类。
    • 在一些情况下,可能会引入性能开销。

案例:

 class Adaptee {
     specificRequest() {
         return '德国标准的插头';
     }
 }
 ​
 class Target {
     constructor() {
         this.adaptee = new Adaptee();
     }
     request() {
         let info = this.adaptee.specificRequest();
         return `${info} -> 转换器 -> 中国标准的插头`
     }
 }
 ​
 // 测试
 let client = new Target();
 client.request();

输出结果:

 德国标准的插头 -> 转换器 -> 中国标准的插头
3.代理模式

在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。

使用者无权访问目标对象,之间加代理,提高代理做授权和控制。

  • 优点

    • 可以控制对对象的访问,实现了访问控制和权限管理。
    • 可以实现延迟加载,提高了性能。
  • 缺点

    • 增加了代码复杂性,因为需要引入代理类。
    • 代理类和真实类之间的关系可能会变得复杂。
 /**
  * pre:代理模式
  * 小明追求A,B是A的好朋友,小明比不知道A什么时候心情好,不好意思直接将花交给A,
  * 于是小明将花交给B,再由B交给A.
  */
 ​
 // 花的类 
 class Flower{
     constructor(name){
         this.name = name 
     }
 }
 ​
 // 小明拥有sendFlower的方法
 let Xioaming = {
     sendFlower(target){
         var flower = new Flower("玫瑰花")
         target.receive(flower)
     }
 }
 // B对象中拥有接受花的方法,同时接收到花之后,监听A的心情,并且传入A心情好的时候函数
 let B = {
     receive(flower){
         this.flower =flower
         A.listenMood(()=>{
             A.receive(this.flower)
         })
     }
 ​
 }
 // A接收到花之后输出花的名字
 let A = {
     receive(flower){
         console.log(`A收到了${flower.name} `)
         // A收到了玫瑰花 
     },
     listenMood(func){
         setTimeout(func,1000)
     }
 }
 Xioaming.sendFlower(B)

常见的行为型模式:

1.策略模式

在策略模式(Strategy Pattern)中一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。

  • 优点

    • 允许动态地切换算法,提高了灵活性。
    • 减少了条件语句,提高了可维护性。
  • 缺点

    • 可能会引入多个策略类,增加了类的数量。
    • 需要客户端选择合适的策略,可能需要额外的决策逻辑。
2.观察者模式

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

  • 优点

    • 实现了对象之间的松耦合,被观察者和观察者之间相互独立。
    • 支持一对多的依赖关系,实现了事件和通知机制。
  • 缺点

    • 如果观察者过多或通知频繁,可能会导致性能问题。
    • 可能需要额外的代码来处理订阅和取消订阅。
3.迭代器模式

迭代器模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。

  • 优点

    • 封装了集合的遍历逻辑,提高了代码的可维护性。
    • 支持多种遍历方式,如正序、倒序、过滤等。
  • 缺点

    • 增加了类的数量,可能会引入额外的复杂性。
    • 在某些情况下,使用语言内置的迭代功能可能更简单。
4.状态模式

在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

  • 优点

    • 将对象的状态封装成独立的类,使状态转换更加清晰和可维护。
    • 减少了条件语句,提高了代码的可读性。
  • 缺点

    • 增加了类的数量,可能会引入额外的复杂性。
    • 对于简单的状态机,可能会显得过于繁琐。