手撕前端常用 7 种设计模式:从原理到实战,附完整代码案例

在前端开发中,设计模式是解决重复问题的 "通用模板"------ 它不直接提供可运行的代码,却能指导我们写出更易维护、可扩展的逻辑。尤其是在大型项目(如组件库开发、状态管理、工具函数封装)中,合理运用设计模式能让代码结构更清晰,协作效率翻倍。

今天就带大家手撕前端最常用的 7 种设计模式,每种模式都从 "核心思想→实际应用场景→完整代码实现" 三个维度拆解,结合 Vue、Axios 等大家熟悉的库举例,让抽象的概念落地到真实开发中。

一、工厂模式:批量创建相似对象的 "生产线"

核心思想

工厂模式的本质是用一个函数(或类)封装对象的创建逻辑,外界无需关心对象的具体构造细节,只需调用工厂方法即可获取实例。就像工厂生产产品,你要一个 "手机",工厂直接给你成品,不用管芯片、屏幕怎么组装。

前端应用场景

  • Vue3 的createApp():通过createApp(App).mount('#app')创建 Vue 实例,无需手动 new Vue,内部封装了实例初始化逻辑。
  • Axios 的axios.create():基于自定义配置(如 baseURL、超时时间)创建新的 axios 实例,避免全局配置污染。
  • 组件库中的 "弹窗工厂":批量创建不同类型的弹窗(警告窗、确认窗、输入窗),统一管理样式和行为。

代码实现:简易 Axios 工厂

javascript 复制代码
// 模拟Axios工厂:根据配置创建请求实例
class AxiosFactory {
  // 静态工厂方法
  static create(config = {}) {
    // 默认配置
    const defaultConfig = {
      baseURL: '',
      timeout: 5000,
      headers: { 'Content-Type': 'application/json' }
    };

    // 合并用户配置与默认配置
    const finalConfig = { ...defaultConfig, ...config };

    // 封装请求方法
    const request = async (options) => {
      const { url, method = 'GET', data = {} } = options;
      try {
        const response = await fetch(`${finalConfig.baseURL}${url}`, {
          method,
          headers: finalConfig.headers,
          timeout: finalConfig.timeout,
          body: method === 'POST' ? JSON.stringify(data) : null
        });
        return await response.json();
      } catch (err) {
        console.error('请求失败:', err);
        throw err;
      }
    };

    // 返回封装后的请求实例
    return {
      get: (url, params) => request({ url, params }),
      post: (url, data) => request({ url, method: 'POST', data })
    };
  }
}

// 使用工厂创建实例
const userApi = AxiosFactory.create({ baseURL: 'https://api.user.com' });
const orderApi = AxiosFactory.create({ baseURL: 'https://api.order.com', timeout: 10000 });

// 调用实例方法(无需关心内部实现)
userApi.get('/info');
orderApi.post('/create', { goodsId: 123 });

二、单例模式:确保全局只有一个实例

核心思想

单例模式要求一个类在整个系统中只能有一个实例 ,且提供一个全局访问点。就像浏览器的window对象、Vue 的Vuex实例,无论在哪里访问,拿到的都是同一个对象,避免重复创建造成资源浪费。

前端应用场景

  • 全局状态管理(Vuex/Pinia):整个应用只有一个 Store 实例,确保状态统一。
  • 弹窗组件(如 Vant 的 Toast、Notify):多次调用Toast('提示'),不会创建多个弹窗,而是复用同一个并更新内容。
  • 全局事件总线(EventBus):避免创建多个总线导致事件监听混乱。

代码实现:ES6 私有属性实现单例

JavaScript 复制代码
class Singleton {
  // 1. 用私有属性(#开头)存储唯一实例,外部无法直接访问
  static #instance;

  // 2. 私有构造函数:防止外部通过new创建实例
  constructor() {
    // 若已有实例,直接抛出错误(严格限制单例)
    if (Singleton.#instance) {
      throw new Error('单例模式不允许重复创建实例,请通过getInstance()获取');
    }
  }

  // 3. 静态方法:全局访问点,确保只创建一个实例
  static getInstance() {
    // 首次调用时创建实例,后续直接返回已存在的实例
    if (!Singleton.#instance) {
      Singleton.#instance = new Singleton();
    }
    return Singleton.#instance;
  }

  // 示例:单例的业务方法
  doSomething() {
    console.log('单例实例的业务逻辑');
  }
}

// 测试:多次调用获取的是同一个实例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

// 错误用法:外部无法通过new创建实例
// const instance3 = new Singleton(); // 抛出错误

三、观察者模式:"一对多" 的依赖通知机制

核心思想

观察者模式定义了对象间的一对多依赖关系:当 "目标对象" 的状态发生改变时,所有依赖它的 "观察者对象" 会自动收到通知并更新。就像订阅报纸 ------ 你(观察者)订阅了《前端日报》(目标),一旦报纸更新,所有订阅者都会收到新报纸。

前端应用场景

  • Vue 的响应式原理:数据(目标)变化时,依赖该数据的 DOM(观察者)自动更新。
  • 事件监听(如addEventListener):DOM 元素(目标)触发 click 事件时,所有绑定的回调函数(观察者)执行。
  • 组件库的自定义事件:父组件监听子组件的@change事件,子组件触发时父组件收到通知。

代码实现:简易 EventBus(观察者模式实践)

javascript 复制代码
class EventBus {
  // 私有属性:存储事件与对应的观察者(回调函数)
  #handlers = {}; // 结构:{ 事件名: [callback1, callback2, ...] }

  // 1. 订阅事件:添加观察者
  $on(eventName, callback) {
    if (typeof callback !== 'function') {
      throw new Error('回调函数必须是function类型');
    }
    // 若事件不存在,初始化空数组
    if (!this.#handlers[eventName]) {
      this.#handlers[eventName] = [];
    }
    // 添加回调到事件列表
    this.#handlers[eventName].push(callback);
  }

  // 2. 触发事件:通知所有观察者
  $emit(eventName, ...args) {
    // 若事件无订阅者,直接返回
    const callbacks = this.#handlers[eventName] || [];
    // 执行所有回调,并传递参数
    callbacks.forEach(callback => callback(...args));
  }

  // 3. 取消订阅:移除观察者
  $off(eventName) {
    if (eventName) {
      // 移除指定事件的所有订阅者
      this.#handlers[eventName] = [];
    } else {
      // 若未传事件名,清空所有订阅
      this.#handlers = {};
    }
  }

  // 4. 一次性订阅:触发后自动取消订阅
  $once(eventName, callback) {
    // 封装回调:执行后立即取消订阅
    const wrapper = (...args) => {
      callback(...args); // 执行原回调
      this.$off(eventName); // 取消订阅
    };
    this.$on(eventName, wrapper);
  }
}

// 使用示例
const bus = new EventBus();

// 订阅事件
bus.$on('userLogin', (username) => {
  console.log(`欢迎 ${username} 登录`);
});

// 一次性订阅
bus.$once('showTip', (msg) => {
  console.log('提示:', msg);
});

// 触发事件
bus.$emit('userLogin', '张三'); // 输出:欢迎 张三 登录
bus.$emit('showTip', '请完善个人资料'); // 输出:提示:请完善个人资料
bus.$emit('showTip', '再次触发'); // 无输出(已取消订阅)

// 取消订阅
bus.$off('userLogin');
bus.$emit('userLogin', '李四'); // 无输出(已取消订阅)

四、发布订阅模式:解耦发布者与订阅者的 "中间件"

核心思想

发布订阅模式是观察者模式的 "升级版",它通过一个 "事件中心" 中间件,让发布者和订阅者完全解耦 ------ 发布者不用知道谁在订阅,订阅者也不用知道谁在发布,两者通过事件中心通信。

举个例子:你(订阅者)在短视频平台订阅了 "前端教程" 标签(事件中心),UP 主(发布者)发布带该标签的视频时,平台(事件中心)会把视频推送给所有订阅该标签的用户。

与观察者模式的区别

维度 观察者模式 发布订阅模式
耦合度 目标与观察者直接依赖 发布者、订阅者无直接依赖
通信方式 目标直接通知观察者 通过事件中心间接通信
适用场景 简单的一对一 / 一对多依赖 复杂系统、跨模块通信

前端应用场景

  • 跨组件通信(如 Vue 的 EventBus、React 的 Context):组件间不直接引用,通过事件中心传递消息。
  • 状态管理库(如 Redux):Action(发布者)触发后,Store(事件中心)通知 Reducer 更新状态,组件(订阅者)感知状态变化。
  • 浏览器的CustomEvent:自定义事件通过dispatchEvent发布,addEventListener订阅,浏览器作为事件中心。

五、原型模式:通过 "复制" 创建新对象

核心思想

原型模式是基于已有对象(原型)创建新对象,新对象会继承原型的属性和方法,避免重复定义相似对象。就像工厂生产玩具 ------ 先做一个 "原型玩具",后续所有玩具都复制原型的样式,再微调细节。

在 JavaScript 中,原型模式是语言的核心特性:每个对象都有__proto__属性,指向它的原型对象,原型链就是基于这个机制实现的。

前端应用场景

  • Object.create():以指定对象为原型,创建新对象(如const obj = Object.create(prototypeObj))。
  • Vue2 的数组方法重写:Vue2 为数组的pushpop等方法添加响应式逻辑,新数组会继承这些重写后的方法。
  • 组件复用:Vue 的组件实例会继承组件选项(如datamethods),本质是原型继承。

代码实现:基于原型的对象创建

javascript 复制代码
// 1. 定义原型对象(被复制的模板)
const userPrototype = {
  // 原型方法
  greet() {
    console.log(`Hello, 我是 ${this.name},年龄 ${this.age}`);
  },
  // 原型属性
  role: 'user'
};

// 2. 基于原型创建新对象(方式1:Object.create)
const user1 = Object.create(userPrototype);
// 为新对象添加自身属性
user1.name = '张三';
user1.age = 20;
user1.greet(); // 输出:Hello, 我是 张三,年龄 20
console.log(user1.role); // 输出:user(继承自原型)

// 3. 基于原型创建新对象(方式2:手动实现原型链)
function createUser(name, age) {
  const user = {};
  // 设置原型:让新对象继承userPrototype
  Object.setPrototypeOf(user, userPrototype);
  // 添加自身属性
  user.name = name;
  user.age = age;
  return user;
}

const user2 = createUser('李四', 22);
user2.greet(); // 输出:Hello, 我是 李四,年龄 22
console.log(user2.__proto__ === userPrototype); // true(验证原型链)

六、代理模式:控制对对象的 "访问权限"

核心思想

代理模式为一个对象提供 "代理" 对象,外界通过代理对象访问原对象,代理可以在访问前后添加额外逻辑(如缓存、权限校验、日志记录)。就像你找房产中介(代理)租房,中介会帮你筛选房源、谈判价格,你不用直接和房东(原对象)打交道。

前端应用场景

  • 缓存代理:第一次请求数据后缓存结果,重复请求直接返回缓存(如本文开头的英雄查询案例)。
  • 权限代理:控制敏感操作的访问权限(如未登录用户无法调用 "提交订单" 接口)。
  • Vue3 的响应式代理:通过Proxy代理对象,拦截属性的读取 / 修改,实现响应式更新。

代码实现:缓存代理(API 请求优化)

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>缓存代理:API请求优化</title>
</head>
<body>
  <h1>城市查询(缓存代理演示)</h1>
  <input type="text" class="query-input" placeholder="输入省份名查询城市(如:广东省)">
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    // 1. 定义缓存对象:存储已查询的结果
    const cache = {};

    // 2. 代理函数:封装API请求,添加缓存逻辑
    async function searchCity(provinceName) {
      // 若缓存中存在,直接返回缓存结果
      if (cache[provinceName]) {
        console.log('从缓存中获取数据');
        return cache[provinceName];
      }

      // 若缓存中不存在,发起API请求
      console.log('从API获取数据');
      try {
        const response = await axios({
          url: 'http://hmajax.itheima.net/api/city',
          params: { pname: provinceName }
        });
        // 缓存结果(键:省份名,值:城市列表)
        cache[provinceName] = response.data.list;
        return response.data.list;
      } catch (err) {
        console.error('查询失败:', err);
        throw err;
      }
    }

    // 3. 绑定DOM事件:用户输入回车后查询
    document.querySelector('.query-input').addEventListener('keyup', async (e) => {
      if (e.keyCode === 13) { // 按下回车键
        const province = e.target.value.trim();
        if (!province) return;
        const cities = await searchCity(province);
        console.log('查询结果:', cities);
      }
    });
  </script>
</body>
</html>

效果验证:第一次输入 "广东省" 会发起 API 请求,第二次输入 "广东省" 会直接从缓存返回结果,减少网络请求次数。

七、迭代器模式:统一 "遍历" 不同数据结构

核心思想

迭代器模式提供一种统一的遍历接口,让外界可以不关心数据结构的内部实现(如数组、对象、Set、Map),用相同的方式遍历不同数据。就像旅游向导(迭代器)带你游览不同景点(数据结构),你不用知道景点的路线,只需跟着向导走。

在 JavaScript 中,迭代器模式通过 "迭代协议" 实现:

  • 可迭代协议 :对象必须有[Symbol.iterator]方法,该方法返回一个迭代器对象。
  • 迭代器协议 :迭代器对象必须有next()方法,每次调用返回{ done: boolean, value: any }done表示是否遍历结束,value表示当前值)。

前端应用场景

  • for...of循环:统一遍历数组、Set、Map、字符串等可迭代对象。
  • 数组的forEach方法:抽象遍历逻辑,用户只需关注每个元素的处理。
  • 自定义数据结构遍历:如链表、树结构,通过迭代器实现统一遍历。

代码实现:自定义对象迭代器

javascript 复制代码
// 定义一个自定义数据结构(包含数组属性)
const customCollection = {
  items: ['前端', 'Java', 'Python', 'UI/UX'], // 待遍历的数据
  length: 4,

  // 1. 实现可迭代协议:添加[Symbol.iterator]方法
  [Symbol.iterator]() {
    let index = 0; // 遍历索引
    const items = this.items; // 保存this指向(避免闭包中this丢失)

    // 2. 实现迭代器协议:返回带有next()方法的迭代器对象
    return {
      next() {
        // 遍历未结束:返回当前值和done=false
        if (index < items.length) {
          return {
            done: false,
            value: items[index++] // 返回当前值后,索引自增
          };
        }
        // 遍历结束:返回done=true
        return { done: true };
      }
    };
  }

  // 可选:用Generator简化迭代器实现(更简洁)
  // [Symbol.iterator]() {
  //   function* generator() {
  //     for (const item of this.items) {
  //       yield item; // 每次yield返回一个值,自动管理索引
  //     }
  //   }
  //   return generator.call(this);
  // }
};

// 3. 用统一方式遍历自定义数据结构(for...of)
for (const item of customCollection) {
  console.log('遍历结果:', item); // 依次输出:前端、Java、Python、UI/UX
}

// 4. 手动调用迭代器(验证迭代器协议)
const iterator = customCollection[Symbol.iterator]();
console.log(iterator.next()); // { done: false, value: '前端' }
console.log(iterator.next()); // { done: false, value: 'Java' }
console.log(iterator.next()); // { done: false, value: 'Python' }
console.log(iterator.next()); // { done: false, value: 'UI/UX' }
console.log(iterator.next()); // { done: true }

总结:设计模式不是 "银弹",而是 "工具箱"

最后想强调:设计模式不是必须遵守的 "规则",而是解决问题的 "工具"。在实际开发中,我们不需要刻意追求 "用满所有模式",而是根据场景选择合适的工具:

  • 需批量创建对象 → 工厂模式

  • 需全局唯一实例 → 单例模式

  • 需事件通知 → 观察者 / 发布订阅模式

  • 需复用对象 → 原型模式

  • 需控制访问 → 代理模式

  • 需统一遍历 → 迭代器模式

但设计模式的世界远不止这 6 种,还有很多 "工具" 等待着我们在特定场景下的邂逅,然后通过这些"工具"能帮我们更加优雅地解决问题,这就是设计模式的魅力所在。 记住:好的代码不是 "用了多少设计模式",而是 "是否清晰解决了问题"。希望这篇文章能帮你理解设计模式的本质,让你的代码在 "可维护" 和 "可扩展" 上更上一层楼。


如果您觉得这篇文章对您有帮助,欢迎点赞和收藏,大家的支持是我继续创作优质内容的动力🌹🌹🌹也希望您能在😉😉😉我的主页 😉😉😉找到更多对您有帮助的内容。

  • 致敬每一位赶路人
相关推荐
TimelessHaze5 分钟前
🔥 一文掌握 JavaScript 数组方法(2025 全面指南):分类解析 × 业务场景 × 易错点
前端·javascript·trae
jvxiao37 分钟前
搭建个人博客系列--(4) 利用Github Actions自动构建博客
前端
袁煦丞1 小时前
SimpleMindMap私有部署团队脑力风暴:cpolar内网穿透实验室第401个成功挑战
前端·程序员·远程工作
li理1 小时前
鸿蒙 Next 布局开发实战:6 大核心布局组件全解析
前端
EndingCoder1 小时前
React 19 与 Next.js:利用最新 React 功能
前端·javascript·后端·react.js·前端框架·全栈·next.js
li理1 小时前
鸿蒙 Next 布局大师课:从像素级控制到多端适配的实战指南
前端
前端赵哈哈1 小时前
Vite 图片压缩的 4 种有效方法
前端·vue.js·vite
Nicholas681 小时前
flutter滚动视图之ScrollView源码解析(五)
前端
电商API大数据接口开发Cris1 小时前
Go 语言并发采集淘宝商品数据:利用 API 实现高性能抓取
前端·数据挖掘·api
风中凌乱的L1 小时前
vue 一键打包上传
前端·javascript·vue.js