JS设计模式指南

196 阅读7分钟

什么是设计模式

简单来说,设计模式就是解决一类问题通用且可复用的方案。它不是具体的代码,而是一种思想,一种经验总结。我们平时写代码时,设计模式就是我们写代码的结构和“地基”,我们要按照这个特定模式来写。因此,在编程中,设计模式就是帮我们写出更健壮且易于维护的代码。

常见设计模式如下:

单例模式

  1. 核心思想:保证一个类只有一个实例,并提供一个全局访问点

试想,你有一个全局的配置管理器,你希望在整个应用中,无论在哪里都只创建一个实例,而不是每次调用都创建一个新的,这就是单例模式的用武之地。

  1. 怎么实现:用一个变量来缓存这个已经创建好的实例

    1. class Singleton {
        constructor() {
          // 假设这里有一些初始化逻辑
          console.log('我被创建了!');
        }
      
        // 核心:静态方法或属性来管理实例
        static getInstance() {
          // 检查是否已经存在实例
          if (!Singleton.instance) {
            // 如果不存在,就创建一个新的并缓存起来
            Singleton.instance = new Singleton();
          }
          // 返回缓存的实例
          return Singleton.instance;
        }
      
        // 假设有一些方法
        showMessage() {
          console.log('我是单例模式的实例!');
        }
      }
      
      // 第一次获取实例
      const instance1 = Singleton.getInstance(); 
      // 输出: "我被创建了!"
      
      // 第二次获取实例
      const instance2 = Singleton.getInstance();
      // 不会再输出 "我被创建了!"
      
      // 比较两个实例,它们是同一个!
      console.log(instance1 === instance2); // 输出: true
      
      instance1.showMessage();
      instance2.showMessage();
      
  2. 总结:单例模式就像一个独一无二的总司令,所有人只能通过一个固定的渠道(getInstance方法)来和他联系,确保不会有第二个总司令出现。

工厂模式

  1. 核心思想:定义一个创建对象的接口,但让子类决定实例化哪一个类。

这个模式就像一个工厂。你告诉工厂你想要一个什么产品,工厂就会帮你生产出来,但不需要知道具体的生产细节。

  1. 怎么实现:我们创建一个工厂函数或工厂类,专门负责创建不同类型的对象。

    1. // 假设我们有不同类型的用户
      class RegularUser {
        constructor() {
          this.type = '普通用户';
        }
      }
      
      class AdminUser {
        constructor() {
          this.type = '管理员';
        }
      }
      
      // 工厂函数:根据传入的类型创建不同的用户对象
      function UserFactory(type) {
        switch (type) {
          case 'regular':
            return new RegularUser();
          case 'admin':
            return new AdminUser();
          default:
            throw new Error('不支持的用户类型!');
        }
      }
      
      // 生产一个普通用户
      const user1 = UserFactory('regular');
      console.log(user1.type); // 输出: "普通用户"
      
      // 生产一个管理员
      const user2 = UserFactory('admin');
      console.log(user2.type); // 输出: "管理员"
      
  2. 总结:工厂模式把创建对象的逻辑和使用对象的逻辑分开了。当你需要创建许多相似但又有点不同的对象时,它可以使代码更整洁的同时且易于扩展。

观察者模式

  1. 核心思想:定义了对象之间一对多的关系,当一个对象状态发生改变时,所有依赖它的对象都会得到通知并自动更新。

这个模式就像公众号推送文章。公众号(主题)推送一篇文章,所有订阅了这个公众号的读者(观察者)都会收到这篇文章。

  1. 怎么实现:需要一个主题来管理观察者,以及一些观察者。

主题:负责添加,移除,通知观察者。

观察者:订阅主题,并提供一个更新方法。

  1. 总结

观察者模式在前端应用中非常常见,比如事件监听(addEventListener).DOM元素就是主题,你注册的回调函数就是观察者。当事件发生时,DOM元素会通知所有注册的函数执行。

代理模式

  1. 核心思想:为另一个对象提供一个替身或占位符,以控制对这个对象的访问。

类比于明星与经纪人,艺人所有事物都由经纪人处理,外部只和经纪人打交道,经纪人决定是否让外部接触到明星

  1. 怎么实现:创建一个代理对象,它和真实对象实现相同的接口,但在真实调用对象的方法前后,以增加额外的逻辑

    1. // 1. 定义真实对象 (明星)
      class Star {
        constructor(name) {
          this.name = name;
        }
      
        // 假设这是明星接广告的方法
        receiveAd(ad) {
          console.log(`${this.name} 正在拍摄广告: ${ad}`);
        }
      }
      
      // 2. 定义代理对象 (经纪人)
      class Agent {
        constructor(star) {
          // 代理对象内部持有真实对象的引用
          this.star = star;
        }
      
        // 代理方法:在调用真实对象的方法前后增加逻辑
        receiveAd(ad) {
          // 前置逻辑:代理人先筛选广告
          if (ad === '劣质广告') {
            console.log('经纪人拒绝了劣质广告!');
            return;
          }
          
          console.log(`经纪人正在为 ${this.star.name} 接洽广告...`);
          
          // 调用真实对象的方法
          this.star.receiveAd(ad);
          
          // 后置逻辑:广告拍摄后,代理人处理后续事务
          console.log('经纪人处理了广告尾款和宣传事宜。');
        }
      }
      
      // --- 使用代理模式 ---
      
      const jayChou = new Star('周杰伦');
      const jayChouAgent = new Agent(jayChou);
      
      // 通过经纪人(代理)来接广告
      jayChouAgent.receiveAd('优质饮品广告');
      // 输出:
      // 经纪人正在为 周杰伦 接洽广告...
      // 周杰伦 正在拍摄广告: 优质饮品广告
      // 经纪人处理了广告尾款和宣传事宜。
      
      console.log('---')
      
      // 经纪人拒绝劣质广告
      jayChouAgent.receiveAd('劣质广告');
      // 输出:
      // 经纪人拒绝了劣质广告!
      
  2. 总结:代理模式关注的是对对象的控制和保护。我需要对一个对象的访问进行控制(在访问的前后做一些事),所以用一个代理包裹它。

装饰器模式

  1. 核心思想:动态地给一个对象添加额外的功能。

想象一下,你有一个基础的咖啡(被装饰对象)。你想给它加点东西,比如牛奶,糖,奶油,你不需要改变咖啡本身的结构,而是用装饰器来包裹它,从而动态地添加新功能

  1. 怎么实现:创建一系列装饰器,它们都继承自统一接口,并且都持有一个被装饰对象的引用。每个装饰器在调用被装饰对象的方法后,再添加自己的功能。

    1. // 1. 定义基础组件 (咖啡)
      class Coffee {
        cost() {
          return 10;
        }
        getDescription() {
          return '一杯纯咖啡';
        }
      }
      
      // 2. 定义装饰器基类 (通常是抽象类或接口,JS中可以用普通类模拟)
      // 装饰器基类应该和Coffee有相同的接口
      // class Decorator {
      //    cost() { ... }
      //    getDescription() { ... }
      // }
      
      // 3. 定义具体的装饰器 (牛奶, 糖, 奶油)
      
      // 牛奶装饰器
      class MilkDecorator {
        constructor(coffee) {
          this.coffee = coffee; // 持有被装饰对象的引用
        }
      
        cost() {
          // 在原有价格上加上牛奶的价格
          return this.coffee.cost() + 3;
        }
      
        getDescription() {
          // 在原有描述上加上牛奶
          return this.coffee.getDescription() + ', 加牛奶';
        }
      }
      
      // 糖装饰器
      class SugarDecorator {
        constructor(coffee) {
          this.coffee = coffee;
        }
      
        cost() {
          return this.coffee.cost() + 2;
        }
      
        getDescription() {
          return this.coffee.getDescription() + ', 加糖';
        }
      }
      
      // --- 使用装饰器模式 ---
      
      let myCoffee = new Coffee(); // 制作一杯纯咖啡
      
      console.log(myCoffee.getDescription(), myCoffee.cost()); // 输出: 一杯纯咖啡 10
      
      // 给咖啡加上牛奶
      myCoffee = new MilkDecorator(myCoffee);
      console.log(myCoffee.getDescription(), myCoffee.cost()); // 输出: 一杯纯咖啡, 加牛奶 13
      
      // 再给这杯加了牛奶的咖啡加上糖
      myCoffee = new SugarDecorator(myCoffee);
      console.log(myCoffee.getDescription(), myCoffee.cost()); // 输出: 一杯纯咖啡, 加牛奶, 加糖 15
      
      // ES7+ 的 Decorator 语法 (更简洁,但目前仍是提案)
      // @milk
      // @sugar
      // class MyCoffee {
      //   ...
      // }
      
  2. 总结:装饰器模式通过组合而非继承的方式来扩展对象的功能。它可以避免创建一个复杂的继承体系。这种方式非常灵活,可以动态地,一层一层地给对象添加功能。像react框架中,高阶组件就是一种典型地装饰器模式体现。

总结思考

设计模式的精髓在于解耦(降低模块间的耦合度)和扩展(让代码更容易添加新功能)。当你面对一个具体问题时,试着思考:"我可以用什么模式解决“。这样就可以把理论知识转为实际的编码能力。