理解编程的设计模式(前端角度)

设计模式(Design Pattern)是在软件开发中,针对反复出现的问题总结出的可复用解决方案,它是对代码设计经验的提炼,目的是提高代码的可维护性、可扩展性和可读性。简单来说,设计模式是 "前人踩坑后总结的最佳实践",不依赖特定语言,但通常通过代码结构体现。

设计模式的分类

根据解决问题的侧重点不同,设计模式通常分为三大类:创建型模式、结构型模式、行为型模式。三者的本质区别在于关注的核心问题不同:

  • 创建型模式:关注如何创建对象 / 实例(创建逻辑的灵活性),目标是解耦对象的创建与使用,隐藏创建细节
  • 结构型模式:关注如何组合对象 / 类(结构的合理性与扩展性),目标是优化对象间的组合关系,使结构更灵活稳定
  • 行为型模式:关注如何处理对象 / 类之间的交互(行为的协调性),目标是规范对象间的通信方式,减少交互耦合
flowchart TD A[设计模式] --> B[创建型模式
关注: 如何创建对象] A --> C[结构型模式
关注: 如何组合对象] A --> D[行为型模式
关注: 对象如何交互] %% 创建型模式 - 工厂模式 B --> B1[工厂模式] B1 --> B1S[场景模块] B1S --> B1S1[UI组件批量创建、数据格式化、相似对象创建] B1 --> B1C[核心实现] B1C --> B1C1[统一工厂函数、根据输入返回对象、隐藏创建细节] %% 创建型模式 - 单例模式 B --> B2[单例模式] B2 --> B2S[场景模块] B2S --> B2S1[全局弹窗/EventBus、全局状态管理、全局配置管理] B2 --> B2C[核心实现] B2C --> B2C1[确保唯一实例、ES6模块/闭包、延迟初始化] %% 创建型模式 - 建造者模式 B --> B3[建造者模式] B3 --> B3S[场景模块] B3S --> B3S1[图表配置、表单配置、复杂对象构建] B3 --> B3C[核心实现] B3C --> B3C1[分步构建、链式调用.build、灵活组合配置] %% 创建型模式 - 原型模式 B --> B4[原型模式] B4 --> B4S[场景模块] B4S --> B4S1[对象克隆、状态更新、复用对象结构] B4 --> B4C[核心实现] B4C --> B4C1[...obj扩展运算符、浅克隆/深克隆、Object.assign] %% 结构型模式 - 装饰器模式 C --> C1[装饰器模式] C1 --> C1S[场景模块] C1S --> C1S1[HOC组件增强、函数添加日志/缓存、权限控制] C1 --> C1C[核心实现] C1C --> C1C1[包装原对象、不修改原代码、功能叠加] %% 结构型模式 - 适配器模式 C --> C2[适配器模式] C2 --> C2S[场景模块] C2S --> C2S1[数据格式转换、接口兼容、第三方库适配] C2 --> C2C[核心实现] C2C --> C2C1[转换接口、数据/方法适配器、保持原接口不变] %% 结构型模式 - 代理模式 C --> C3[代理模式] C3 --> C3S[场景模块] C3S --> C3S1[数据校验/缓存、Vue响应式、权限控制] C3 --> C3C[核心实现] C3C --> C3C1[Proxy拦截、访问前后插入逻辑、get/set拦截器] %% 结构型模式 - 组合模式 C --> C4[组合模式] C4 --> C4S[场景模块] C4S --> C4S1[多级菜单、树形结构、组件树] C4 --> C4C[核心实现] C4C --> C4C1[统一接口、递归处理子节点、叶子/组合节点] %% 结构型模式 - 外观模式 C --> C5[外观模式] C5 --> C5S[场景模块] C5S --> C5S1[复杂初始化、第三方库封装、跨模块协作] C5 --> C5C[核心实现] C5C --> C5C1[简化入口、封装子系统调用、隐藏复杂性] %% 行为型模式 - 观察者模式 D --> D1[观察者模式] D1 --> D1S[场景模块] D1S --> D1S1[主题切换、组件状态联动、异步事件通知] D1 --> D1C[核心实现] D1C --> D1C1[Subject/Observer、数组存储观察者、subscribe/notify] %% 行为型模式 - 发布订阅模式 D --> D2[发布订阅模式] D2 --> D2S[场景模块] D2S --> D2S1[跨组件通信、全局状态联动、事件中心] D2 --> D2C[核心实现] D2C --> D2C1[事件中心、对象存储事件映射、publish/subscribe] %% 行为型模式 - 策略模式 D --> D3[策略模式] D3 --> D3S[场景模块] D3S --> D3S1[表单验证、支付方式选择、数据格式化] D3 --> D3C[核心实现] D3C --> D3C1[策略映射表、替代if-else、动态切换策略] %% 行为型模式 - 状态模式 D --> D4[状态模式] D4 --> D4S[场景模块] D4S --> D4S1[弹窗状态、多步骤表单、游戏角色状态] D4 --> D4C[核心实现] D4C --> D4C1[状态对象、行为委托、状态转换驱动] %% 行为型模式 - 命令模式 D --> D5[命令模式] D5 --> D5S[场景模块] D5S --> D5S1[撤销/重做、批量操作、延迟执行] D5 --> D5C[核心实现] D5C --> D5C1[命令对象、execute/undo方法、命令队列管理] %% 样式 classDef creational fill:#e1f5ff,stroke:#01579b,stroke-width:2px classDef structural fill:#f3e5f5,stroke:#4a148c,stroke-width:2px classDef behavioral fill:#fff3e0,stroke:#e65100,stroke-width:2px classDef scene fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px classDef core fill:#fff9c4,stroke:#f57f17,stroke-width:1px class B,B1,B2,B3,B4 creational class C,C1,C2,C3,C4,C5 structural class D,D1,D2,D3,D4,D5 behavioral class B1S,B1S1,B1S2,B1S3,B2S,B2S1,B2S2,B2S3,B3S,B3S1,B3S2,B3S3,B4S,B4S1,B4S2,B4S3 scene class C1S,C1S1,C1S2,C1S3,C2S,C2S1,C2S2,C2S3,C3S,C3S1,C3S2,C3S3,C4S,C4S1,C4S2,C4S3,C5S,C5S1,C5S2,C5S3 scene class D1S,D1S1,D1S2,D1S3,D2S,D2S1,D2S2,D2S3,D3S,D3S1,D3S2,D3S3,D4S,D4S1,D4S2,D4S3,D5S,D5S1,D5S2,D5S3 scene class B1C,B1C1,B1C2,B1C3,B2C,B2C1,B2C2,B2C3,B3C,B3C1,B3C2,B3C3,B4C,B4C1,B4C2,B4C3 core class C1C,C1C1,C1C2,C1C3,C2C,C2C1,C2C2,C2C3,C3C,C3C1,C3C2,C3C3,C4C,C4C1,C4C2,C4C3,C5C,C5C1,C5C2,C5C3 core class D1C,D1C1,D1C2,D1C3,D2C,D2C1,D2C2,D2C3,D3C,D3C1,D3C2,D3C3,D4C,D4C1,D4C2,D4C3,D5C,D5C1,D5C2,D5C3 core

创建型模式(Creational Patterns)

创建型模式(Creational Patterns)核心解决 "对象创建" 的问题------ 即如何灵活、高效地创建对象,同时隐藏创建细节,降低代码耦合。当对象的创建逻辑复杂(如依赖条件判断、需要控制实例数量、或需复用创建过程)时,创建型模式能让对象的创建与使用解耦,提升代码的可维护性和扩展性。

创建型模式的核心目标:

  • 隐藏创建细节:使用者无需知道对象是如何被创建的(如不需要关心构造函数参数、实例化条件等)。
  • 灵活控制创建:可根据场景动态切换创建逻辑(如不同环境创建不同配置的对象)。
  • 减少重复代码:将重复的创建逻辑封装,避免在多处复制粘贴。

在前端开发中,创建型模式的应用往往与组件化、状态管理、工具函数设计紧密相关。由于前端更关注 "灵活创建实例 / 组件""控制全局资源""简化复杂对象构建",所以常用的创建型模式包括:工厂模式、单例模式、建造者模式、原型模式。

  • 工厂模式:用统一 "工厂" 根据输入创建不同类型的对象,隐藏创建细节。
  • 单例模式:确保一个类只有且仅有一个实例,提供全局唯一访问点。
  • 建造者模式:将复杂对象的创建拆分成步骤,分步构建后组装成完整对象。
  • 原型模式:通过克隆已有对象(原型)创建新对象,复用结构并减少重复初始化。

1. 工厂模式(Factory Pattern)

工厂模式的核心是通过一个 "工厂" 统一管理对象 / 组件的创建逻辑,让使用者无需关心具体实现细节,只需专注于 "用" 而非 "造"。这种模式在组件化开发、数据处理、工具函数设计等场景中应用广泛,能显著提升代码的复用性和可维护性。

前端开发中,我们经常遇到这样的问题:

  • 需要创建多个结构相似的组件(如不同类型的按钮、表单输入框);
  • 数据格式需要统一转换(如接口返回的原始数据→页面展示数据);
  • 复杂对象的创建逻辑分散在各处,修改时需要改多个地方。

工厂模式通过 "集中创建逻辑" 解决这些问题,举个最简单的例子:如果页面需要 10 个不同样式的按钮,直接写 10 遍会很冗余;但用工厂模式封装后,只需调用ButtonFactory(type)就能生成对应按钮,既简洁又便于统一修改样式。

根据创建逻辑的复杂度,前端常用的工厂模式分为两类:

  • 简单工厂模式(最常用),用一个函数搞定 "相似对象批量创建",适合大多数日常场景;
  • 工厂方法模式(复杂场景),用 "抽象 + 子类" 应对 "多类型、需扩展" 的复杂场景,适合框架 / 库开发。

1.1 简单工厂模式(Simple Factory Pattern)

简单工厂模式的核心:用一个函数 / 对象作为 "工厂",根据输入参数直接返回不同类型的对象 / 组件,逻辑集中在单一工厂中。

适用场景:创建的对象类型较少、逻辑简单,且不需要频繁扩展新类型(如基础 UI 组件、数据格式化)。

实践 1:UI 组件工厂(React/Vue 场景)在组件化开发中,用工厂函数批量生成相似组件,避免重复写样式和事件逻辑。

jsx 复制代码
// React组件工厂:生成不同类型的按钮
function ButtonFactory({ type, label, onClick }) {
  // 基础样式和事件(统一封装)
  const baseStyles = {
    padding: '8px 16px',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer',
  };

  // 根据type参数返回不同样式的按钮
  switch (type) {
    case 'primary':
      return (
        <button style={{ ...baseStyles, background: '#1890ff', color: 'white' }} onClick={onClick}>
          {label}
        </button>
      );
    case 'danger':
      return (
        <button style={{ ...baseStyles, background: '#ff4d4f', color: 'white' }} onClick={onClick}>
          {label}
        </button>
      );
    case 'text':
      return (
        <button
          style={{ ...baseStyles, background: 'transparent', color: '#1890ff' }}
          onClick={onClick}
        >
          {label}
        </button>
      );
    default:
      throw new Error(`不支持的按钮类型:${type}`);
  }
}

// 使用工厂:一行代码创建不同按钮
function Page() {
  return (
    <div>
      <ButtonFactory type="primary" label="提交" onClick={() => console.log('提交')} />
      <ButtonFactory type="danger" label="删除" onClick={() => console.log('删除')} />
      <ButtonFactory type="text" label="取消" onClick={() => console.log('取消')} />
    </div>
  );
}

实践 2:数据格式化工厂接口返回的原始数据往往不符合页面展示需求(如日期格式、空值处理),用工厂函数统一转换,避免在组件中散落格式化逻辑。

js 复制代码
// 数据工厂:格式化用户列表数据
function userDataFactory(rawUser) {
  return {
    // 处理空值:默认显示"未知"
    username: rawUser.name || '未知用户',
    // 格式化日期:时间戳→本地字符串
    registerTime: rawUser.regTime ? new Date(rawUser.regTime).toLocaleString() : '未注册',
    // 处理头像:默认图替代null
    avatar: rawUser.avatarUrl || '/default-avatar.png',
    // 保留原始ID(不修改)
    id: rawUser.id,
  };
}

// 使用:批量格式化接口数据
async function fetchUsers() {
  const rawData = await api.get('/users'); // 接口返回原始数据
  // 用工厂转换后再传给组件
  const formattedUsers = rawData.map((user) => userDataFactory(user));
  setUsers(formattedUsers);
}

1.2 工厂方法模式(Factory Method Pattern)

核心:将工厂抽象化,每个 "产品" 对应一个专门的 "工厂子类",由子类负责具体创建逻辑。父类只定义接口,不关心具体实现。

适用场景:需要创建的对象类型较多、且可能频繁扩展新类型(如插件系统、多支付方式集成),更符合 "开闭原则"(新增类型时无需修改已有代码)。

实践:多支付方式工厂(前端支付场景)假设一个商城需要支持微信、支付宝、银联三种支付方式,且未来可能新增更多方式,用工厂方法模式可灵活扩展。

js 复制代码
// 示例:用户选择微信支付
handlePay(new WechatFactory(), 199); // 微信支付:199元(调用微信SDK)

// 支付方法的参数只有工厂和金额,无需关心具体实现
function handlePay(factory, amount) {
  const payment = factory.createPayment();
  payment.pay(amount);
}

// 1. 抽象产品:定义支付方式的统一接口
class Payment {
  pay(amount) {
    throw new Error('子类必须实现pay方法');
  }
}

// 2. 具体产品:各支付方式的实现
class WechatPayment extends Payment {
  pay(amount) {
    console.log(`微信支付:${amount}元(调用微信SDK)`);
  }
}

class AlipayPayment extends Payment {
  pay(amount) {
    console.log(`支付宝支付:${amount}元(调用支付宝SDK)`);
  }
}

class UnionPayPayment extends Payment {
  pay(amount) {
    console.log(`银联支付:${amount}元(调用银联SDK)`);
  }
}

// 3. 抽象工厂:定义创建支付方式的接口
class PaymentFactory {
  createPayment() {
    throw new Error('子类必须实现createPayment方法');
  }
}

// 4. 具体工厂:每个支付方式对应一个工厂
class WechatFactory extends PaymentFactory {
  createPayment() {
    return new WechatPayment();
  }
}

class AlipayFactory extends PaymentFactory {
  createPayment() {
    return new AlipayPayment();
  }
}

class UnionPayFactory extends PaymentFactory {
  createPayment() {
    return new UnionPayPayment();
  }
}

这里如果你的需求很简单(比如只有 2 种支付方式,且未来不会扩展),简单工厂甚至直接写死逻辑更高效;但如果是开发一个组件库、支付 SDK、插件系统等需要长期维护、可能不断扩展的功能,工厂方法模式的 "前期投入" 能显著降低后期维护成本。

js 复制代码
// 特别简单的话,映射一下就完事了
const paymentFactory = {
  wechat: { pay: (amount) => console.log(`微信支付${amount}元`) },
  alipay: { pay: (amount) => console.log(`支付宝支付${amount}元`) },
  unionpay: { pay: (amount) => console.log(`银联支付${amount}元`) },
};

// 使用
function handlePay(type, amount) {
  const payment = paymentFactory(type);
  payment.pay(amount);
}
handlePay('wechat', 199); // 微信支付199元

但如果是开发一个组件库、支付 SDK、插件系统等需要长期维护、可能不断扩展的功能,工厂方法模式的 "前期投入" 能显著降低后期维护成本。工厂方法模式的价值,在于当需求膨胀到一定程度时,它能让代码依然保持有序。

用React的createElement来深入理解工厂方法模式

React 的 createElement 是工厂方法模式在前端框架中的经典应用。它的核心作用是根据输入的 "标签类型"(如原生 DOM 标签、自定义组件、Fragment 等),创建对应的虚拟 DOM 对象(VNode),同时隐藏不同类型节点的创建细节。从工厂方法模式的角度来看,createElement 本质是一个 "抽象工厂接口",而针对不同节点类型的创建逻辑,则对应了 "具体工厂" 的实现。

首先看下,如果用简单工厂模式来实现createElement

js 复制代码
// 假设用简单工厂实现(伪代码)
function createElement(type, props, ...children) {
  if (typeof type === 'string') {
    // 处理原生DOM节点
    return { type, props: { ...props, children } };
  } else if (typeof type === 'function' || typeof type === 'class') {
    // 处理自定义组件
    return { type, props: { ...props, children } };
  } else if (type === Symbol(react.fragment)) {
    // 处理Fragment
    return { type, props: { ...props, children } };
  } else if (/* 其他类型 */) {
    // ... 几十种分支
  }
}

这种方式在节点类型较少时可行,但 React 支持的节点类型不断扩展(如 Portal、ServerComponent 等),if-else 分支会无限膨胀。

现在用工厂方法模式来实现createElement,注意这里是使用了工厂模式的思想"封装对象创建逻辑,根据输入返回统一接口的不同产品"。但前端是动态类型语言,没有教条化的非得使用类,更倾向于用函数和对象实现相同的逻辑。

js 复制代码
// 用于标记 React 元素的唯一符号(避免与其他对象混淆)
const REACT_ELEMENT_TYPE = Symbol.for('react.element');

/**
 * 创建 React 元素(虚拟 DOM)
 * @param {string|function|Symbol} type 节点类型(如 'div'、组件函数、Fragment)
 * @param {object|null} props 节点属性(如 className、onClick)
 * @param {...any} children 子节点(可以是文本、其他元素等)
 * @returns {object} 虚拟 DOM 对象(VNode)
 */
function createElement(type, props, ...children) {
  // 1. 处理默认参数(如果 props 为 null,转为空对象)
  const propsObj = props || {};

  // 2. 提取 key 和 ref(这两个属性比较特殊,需要单独处理)
  const key = propsObj.key !== undefined ? propsObj.key : null;
  const ref = propsObj.ref !== undefined ? propsObj.ref : null;

  // 3. 处理 props:过滤掉 key 和 ref,剩下的作为普通属性
  const filteredProps = {};
  for (const propName in propsObj) {
    // 排除 key 和 ref(它们会被单独存储)
    if (propName !== 'key' && propName !== 'ref') {
      filteredProps[propName] = propsObj[propName];
    }
  }

  // 4. 标准化子节点(将子节点统一处理为数组,支持文本、嵌套元素等)
  filteredProps.children = normalizeChildren(children);

  // 5. 创建并返回虚拟 DOM 对象(VNode)
  return {
    $$typeof: REACT_ELEMENT_TYPE, // 标识这是一个 React 元素
    type: type, // 节点类型(如 'div'、MyComponent)
    props: filteredProps, // 处理后的属性(包含 children)
    key: key, // 用于列表渲染的 key
    ref: ref, // 用于获取 DOM 或组件实例的 ref
    _owner: null, // 内部属性:记录创建该元素的组件(简化版忽略)
  };
}

/**
 * 标准化子节点:将任意形式的 children 转为统一的数组格式
 * @param {array} children 原始子节点(可能是文本、元素、数组嵌套等)
 * @returns {array} 标准化后的子节点数组
 */
function normalizeChildren(children) {
  const result = [];
  // 遍历所有子节点,处理嵌套数组
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    if (Array.isArray(child)) {
      // 如果子节点是数组,递归处理(支持嵌套数组)
      result.push(...normalizeChildren(child));
    } else {
      // 非数组直接添加(支持文本、null、undefined 等)
      result.push(child);
    }
  }
  return result;
}

createElement 的核心作用是 "标准化输入,输出统一结构的虚拟 DOM"。它隐藏了不同节点类型(原生标签、组件、Fragment 等)的创建细节,最终返回的 VNode 结构一致。

注意事项

  • 不要过度设计:简单场景(如创建 1-2 种对象)直接new或写普通函数即可,无需强行套工厂模式(避免 "为模式而模式")。
  • 优先选择简单工厂:前端业务开发中,简单工厂(单一函数)的性价比最高,代码简洁且足够用;工厂方法模式更适合组件库、插件系统等复杂场景。
  • 结合前端特性简化实现:用 ES6 语法(箭头函数、对象解构、类)简化工厂逻辑,例如用对象映射替代switch
js 复制代码
// 用对象映射简化简单工厂(创建图标组件)
const IconFactory = {
  success: () => <i className="icon-success" />,
  error: () => <i className="icon-error" />,
  warning: () => <i className="icon-warning" />,
};

// 使用:直接通过key获取
function showIcon(type) {
  return IconFactory[type]?.() || <i className="icon-default" />;
}

2. 单例模式(Singleton Pattern)

单例模式的核心是确保一个类 / 对象在全局只有且仅有一个实例,并提供唯一的访问入口。这种模式特别适合管理全局资源(如弹窗、事件总线、全局状态等),避免重复创建导致的资源浪费或逻辑冲突。

前端场景中,很多资源需要 "全局唯一":

  • 全局弹窗管理器:如果重复创建多个,可能导致遮罩层叠加、弹窗混乱;
  • 事件总线(EventBus):多实例会导致事件监听分散,无法跨组件通信;
  • 全局状态存储:多实例会造成状态不一致,组件同步失败。

单例模式通过 "限制实例数量" 解决这些问题,确保全局资源的唯一性和一致性。

2.1 基础类实现单例模式(最经典)

通过类的静态属性存储唯一实例,构造函数中拦截重复创建:

js 复制代码
class Singleton {
  static instance = null;
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
  }

  getInstance() {
    return Singleton.instance;
  }
}

const singleton = new Singleton();
const singleton2 = new Singleton();
console.log(singleton === singleton2); // true

2.2 使用闭包实现单例模式

用闭包封装实例,避免暴露类和静态属性,更符合前端函数式编程习惯:

js 复制代码
const createEventBus = (() => {
  // 闭包变量:存储唯一实例
  let instance;

  // 实际的事件总线类
  class EventBus {
    constructor() {
      this.events = {}; // 存储事件监听
    }

    on(type, callback) {
      this.events[type] = this.events[type] || [];
      this.events[type].push(callback);
    }

    emit(type, data) {
      this.events[type]?.forEach((cb) => cb(data));
    }
  }

  // 返回一个函数,确保每次调用都返回同一个实例
  return () => {
    if (!instance) {
      instance = new EventBus();
    }
    return instance;
  };
})();

// 测试:多次调用,获取同一个实例
const bus1 = createEventBus();
const bus2 = createEventBus();
console.log(bus1 === bus2); // true

// 跨实例通信:bus2 监听,bus1 触发,能收到事件
bus2.on('test', (data) => console.log('收到:', data));
bus1.emit('test', 'hello'); // 输出:收到:hello

模块模式(ES6 Module)

利用 ES6 模块 "加载时执行一次,后续导入共享同一个实例" 的特性,天然实现单例:

js 复制代码
// store.js(模块文件)
class GlobalStore {
  constructor() {
    this.state = { count: 0 };
  }

  increment() {
    this.state.count++;
  }
}

// 模块导出时创建唯一实例
export default new GlobalStore();

// 使用的时候
// 组件A.js
import store from './store.js';
store.increment();
console.log(store.state.count); // 1

// 组件B.js
import store from './store.js';
// 直接读取到组件A修改后的状态

这是前端最推荐的单例实现方式:简单、无冗余代码,利用模块机制天然保证唯一性。但是如果有额外的参数需要传递,比如配置信息,或者需要动态创建实例,就需要上面的基础类实现单例模式。

在很多简单场景下,直接用一个对象(或模块导出的对象)就能能替代单例模式,甚至更简洁。但单例模式(尤其是基于类的实现)和 "直接用对象" 的核心区别在于:是否需要 "延迟初始化" 和 "封装复杂逻辑"。按需设置。

使用场景

  • 全局数据存储:如 localStorage、sessionStorage;
  • 全局弹窗 / 提示框
  • 全局状态管理:如 Redux、Vuex 等;
  • 全局事件总线:如 EventBus;
  • 全局工具函数:如工具函数库;axios/日志等工具函数
  • 全局配置管理:如配置中心;

3. 建造者模式(Builder Pattern)

建造者模式(Builder Pattern)是一种专注于 "分步构建复杂对象"的创建型模式。它将复杂对象的创建过程拆分成多个独立的步骤,通过专门的 "建造者" 来管理这些步骤,最终通过统一的接口组装出完整对象。这种模式特别适合处理配置项繁多、结构复杂 的场景(如表单配置、图表初始化、富文本编辑器参数等),能让创建逻辑更清晰、更灵活。

前端经常遇到需要创建 "复杂对象" 的场景:

  • 一个图表组件可能需要配置标题、坐标轴、数据源、图例、交互事件等十几项参数;
  • 一个表单可能需要设置字段类型、验证规则、布局方式、提交逻辑等;
  • 这些对象的参数不仅多,还可能有依赖关系(如 "是否显示图例" 依赖 "是否有数据")。
  • 如果直接用一个巨大的构造函数或对象字面量创建,会导致:
    • 参数混乱,难以阅读(一个函数可能有 20 个参数);
    • 配置逻辑分散,修改时需要在一堆代码中找对应位置;
    • 无法灵活组合不同配置(如 "基础图表 + 自定义图例""基础图表 + 自定义交互")。 建造者模式通过 "分步设置 + 链式调用" 解决这些问题,让复杂对象的创建过程变得可控且清晰。

建造者模式的核心结构

建造者模式的核心结构包括:

  • 产品(Product):最终要创建的复杂对象(如图表配置、表单配置)。前端更倾向于用对象来表示。
  • 抽象建造者(Builder):定义构建复杂对象的接口,规范各个步骤的实现;前端常用一个类或对象,包含多个设置方法
  • 具体建造者(Concrete Builder 可选):实现抽象建造者的具体步骤,负责构建复杂对象的各个部分;
  • 指挥者(Director 可选):负责调用具体建造者构建复杂对象,控制构建过程的顺序和逻辑;

实践:图表配置建造者

如图表(ECharts、Chart.js)、富文本编辑器(TinyMCE)等,参数繁多且有依赖关系,用建造者模式分步配置更清晰。用建造者模式来实现图表配置,可以更清晰地管理图表的各个部分,并且可以更灵活地组合不同的配置。

js 复制代码
// 看下使用方式
// ECharts配置示例(简化)
const option = new ChartBuilder()
  .setTitle('销量分析')
  .setData([100, 200])
  .setAxis(true, true)
  .setLegend(true)
  .enableClickInteraction()
  // 完成构建,返回最终配置
  .build();
chart.setOption(option);

// 产品:ChartConfig
// 1. 产品:图表配置对象(最终要创建的复杂对象)
class ChartConfig {
  constructor() {
    this.title = '';
    this.data = [];
    this.axis = { x: true, y: true };
    this.legend = false;
    this.interaction = { click: false };
  }
}

// 抽象建造者:ChartBuilder
// 定义构建复杂对象的接口,规范各个步骤的实现
class ChartBuilder {
  constructor() {
    this.config = new ChartConfig(); // 初始化产品
  }

  // 步骤:设置标题
  setTitle(title) {
    this.config.title = title;
    return this; // 返回this,支持链式调用
  }

  // 步骤:设置数据源
  setData(data) {
    this.config.data = data;
    // 有数据时默认显示图例
    if (data.length > 0) {
      this.config.legend = true;
    }
    return this;
  }

  // 步骤:配置坐标轴
  setAxis(xVisible, yVisible) {
    this.config.axis = { x: xVisible, y: yVisible };
    return this;
  }

  // 步骤:开启点击交互
  enableClickInteraction() {
    this.config.interaction.click = true;
    return this;
  }

  // 完成构建,返回最终配置
  build() {
    return this.config;
  }
}

// 前端还可以轻量版
// 轻量版建造者(用对象而非类)
const chartBuilder = () => {
  const config = {
    /* 默认配置 */
  };
  return {
    setTitle: (title) => {
      config.title = title;
      return this;
    },
    setData: (data) => {
      /* ... */ return this;
    },
    build: () => config,
  };
};

// 指挥者:封装常用的图标配置模板(简单的话也可以直接用映射表)
class ChartDirector {
  // 模板1:基础折线图(无交互,显示XY轴)
  static createBasicLineChart(builder, data) {
    return builder
      .setTitle('基础折线图')
      .setData(data)
      .setAxis(true, true) // 显示XY轴
      .build(); // 不开启交互
  }

  // 模板2:带点击交互的柱状图
  static createInteractiveBarChart(builder, data) {
    return builder
      .setTitle('交互柱状图')
      .setData(data)
      .setAxis(true, false) // 只显示X轴
      .enableClickInteraction()
      .build();
  }
}
// 使用指挥者快速创建配置
const builder = new ChartBuilder();

// 创建基础折线图
const lineChartConfig = ChartDirector.createBasicLineChart(builder, [50, 80, 120]);

// 创建交互柱状图
const barChartConfig = ChartDirector.createInteractiveBarChart(builder, [30, 60, 90]);

建造者模式的核心价值:"分步构建,灵活组合"。它让复杂对象的创建过程变得可控且清晰,同时支持灵活组合不同配置,满足各种业务需求。如果是简单对象,直接用对象字面量更简洁。

4. 原型模式(Prototype Pattern)

原型模式(Prototype Pattern)是一种专注于 "通过复制现有对象来创建新对象"的创建型模式 ,其实就是{...obj,name: 'new name'}。 对,这就是原型模式。

前端原型模式的核心是 "以已有对象为模板,通过克隆快速创建新对象",充分利用了 JavaScript 的原型特性(如对象字面量、扩展运算符),避免了重复定义结构的冗余代码。无论是处理列表数据、复用组件配置,还是管理状态更新,原型模式都能通过 "克隆" 提升代码效率和可维护性 ------ 但要注意浅克隆和深克隆的区别!

结构型模式(Structural Pattern)

结构型模式(Structural Pattern)核心解决 "如何组合对象 / 类" 的问题 ------ 即如何更灵活、高效地组织对象 / 类之间的关系,使系统更稳定、可扩展。

简单说,创建型模式关注 "如何创建对象",行为型模式关注 "对象如何交互",而结构型模式关注 "对象如何组合",目的是通过优化结构让系统更易于扩展和维护。

前端开发中,结构型模式多与组件复用、接口适配、功能扩展相关,常用的结构型模式包括:

  • 装饰器模式:动态扩展功能(如 HOC);
  • 适配器模式:解决接口不兼容问题(如数据格式转换);
  • 代理模式:控制对象访问(如缓存、权限);
  • 组合模式:统一处理树形结构(如菜单、目录);
  • 外观模式:简化复杂系统的使用(如 API 封装)。

1. 装饰器模式(Decorator Pattern)

装饰器模式(Decorator Pattern)是一种在不修改原有对象代码的前提下,通过 "包装" 动态为对象添加新功能的结构型模式。它的核心是 "扩展功能而非修改",既保留原对象的完整性,又能灵活叠加新能力,非常适合处理 "功能动态组合" 场景(如给组件加日志、权限控制、缓存等)。

想象你有一件基础 T 恤(原对象),你可以通过加外套、戴围巾、配帽子(装饰器)来扩展它的功能,而 T 恤本身并没有被修改。

  • 原对象(Component):被装饰的基础对象(如基础组件、基础函数);
  • 装饰器(Decorator):包装原对象的 "增强器",包含新功能,同时保留原对象的接口;
  • 核心原则:装饰器和原对象实现相同的接口(或兼容的方法),确保使用者无需区分 "原对象" 和 "装饰后对象"。

前端中,装饰器模式多以函数 / 高阶组件的形式实现,以下是常见场景:

1. 函数装饰:给普通函数添加功能,给函数添加日志、性能统计、错误捕获等功能,且不修改原函数代码。

js 复制代码
// 原函数:基础接口请求
function fetchData(url) {
  return fetch(url).then((res) => res.json());
}

// 装饰器1:添加请求日志
function withLog(fn) {
  // 返回一个新函数(包装原函数)
  return async function (...args) {
    const url = args[0];
    console.log(`开始请求:${url}`); // 新增日志功能
    const result = await fn(...args); // 调用原函数
    console.log(`请求完成:${url}`);
    return result; // 保持原函数的返回值
  };
}

// 装饰器2:添加错误捕获
function withErrorHandling(fn) {
  return async function (...args) {
    try {
      return await fn(...args);
    } catch (err) {
      console.error('请求失败:', err); // 新增错误处理
      return null; // 错误时返回默认值
    }
  };
}

// 装饰原函数:叠加日志和错误处理功能
const decoratedFetch = withErrorHandling(withLog(fetchData));

// 使用:和原函数调用方式完全一致,但多了日志和错误处理
decoratedFetch('https://api.example.com/data');

2. 组件装饰:React 高阶组件(HOC),在 React 中,高阶组件(HOC)是装饰器模式的典型应用------ 通过包装组件,为其添加状态、属性或行为。

js 复制代码
// 示例:给按钮组件添加 "点击埋点" 和 "权限控制"

// 原组件:基础按钮
function Button({ onClick, children, disabled }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  );
}

// 装饰器1:添加点击埋点
function withClickTracking(WrappedComponent) {
  // 返回新组件(包装原组件)
  return function (props) {
    const handleClick = () => {
      // 新增埋点功能
      console.log(`埋点:${props.trackId} 被点击`);
      props.onClick?.(); // 保留原点击逻辑
    };
    // 传递所有属性给原组件
    return <WrappedComponent {...props} onClick={handleClick} />;
  };
}

// 装饰器2:添加权限控制(无权限则禁用)
function withPermission(WrappedComponent) {
  return function (props) {
    const hasPermission = props.userRole === 'admin'; // 权限判断
    // 新增禁用逻辑
    return <WrappedComponent {...props} disabled={!hasPermission} />;
  };
}

// 装饰原组件:叠加埋点和权限控制
const EnhancedButton = withPermission(withClickTracking(Button));

// 使用:和原组件用法一致,但多了埋点和权限控制
<EnhancedButton userRole="admin" trackId="submit-btn" onClick={() => console.log('提交')}>
  提交
</EnhancedButton>;

3. ES7 装饰器语法(语法糖) ,ES7 装饰器语法(如 @withLog@withErrorHandling)是装饰器模式的语法糖,让装饰器写法更简洁。ES7 装饰器目前还是提案阶段,实际项目中需配置相关工具(如 @babel/plugin-proposal-decorators)。

js 复制代码
// 装饰器:给类添加log方法
function withLogger(target) {
  target.prototype.log = function (message) {
    console.log(`[${this.name}] ${message}`);
  };
}

// 用@语法装饰类
@withLogger
class User {
  constructor(name) {
    this.name = name;
  }
}

// 使用:User类自动获得log方法
const user = new User('张三');
user.log('登录成功'); // 输出:[张三] 登录成功

装饰器模式vs继承:装饰器模式可以动态叠加,继承无法动态组合,多层继承可能导致类爆炸。

前端更爱用装饰器:因为组件 / 函数的功能组合往往是动态的(如 "有时需要日志,有时不需要"),装饰器的灵活性远超继承。

前端装饰器模式的典型应用场景

  • 功能扩展:给组件 / 函数添加日志、埋点、缓存、节流防抖等通用功能;
  • 权限控制:根据用户角色动态禁用 / 启用组件功能;
  • 状态增强:给无状态组件添加状态(如 React HOC 给函数组件添加 state);
  • 兼容性处理:为旧接口添加新功能,同时兼容旧用法(如给 fetch 包装 XMLHttpRequest 的兼容逻辑)。

使用装饰器模式的注意事项

  • 保持接口一致:装饰器返回的对象 / 组件必须和原对象接口兼容(如原函数返回 Promise,装饰器也应返回 Promise),否则使用者会混淆;
  • 避免过度装饰:多层装饰可能导致调试困难(如 withA(withB(withC(...)))),必要时可拆分或用其他模式替代;
  • 优先用组合而非装饰:如果功能是核心逻辑(而非扩展),直接在原对象中实现更合适,装饰器适合 "锦上添花" 的功能。

2. 适配器模式(Adapter Pattern)

适配器模式(Adapter Pattern)核心作用是解决 "接口不兼容" 问题------ 通过创建一个 "适配器",将一个对象的接口(方法、数据格式等)转换为另一个对象能理解的接口,让原本因接口不匹配而无法协作的对象可以一起工作。

简单说,适配器就像 "插头转换器":中国的插头(原接口)不能直接插在欧洲的插座(目标接口)上,通过转换器(适配器)就能兼容。

前端开发中,"接口不兼容" 是常见问题:

  • 后端返回的数据格式和前端组件所需格式不一致(如后端用 user_name,前端组件需要 username);
  • 旧系统的 API 和新系统的调用方式不匹配(如旧方法返回 callback,新系统用 Promise);
  • 引入的第三方库 API 和项目现有逻辑不兼容(如第三方日历组件的日期格式和项目的 YYYY-MM-DD 不匹配)。

适配器模式通过 "转换接口" 解决这些问题,避免为了兼容而修改原有代码(符合 "开闭原则")。

根据适配的对象不同,前端适配器主要有两种形式:数据适配器和方法适配器。

1. 数据适配器:转换数据格式 最常见的场景是 "后端接口数据" 与 "前端组件需求" 的格式适配。

js 复制代码
// 后端返回的用户数据(原格式)
const rawUser = {
  user_id: 1001,
  user_name: '张三',
  user_age: 25,
  reg_time: 1620000000000, // 时间戳
};

// 前端组件需要的数据格式(目标格式):
// { id, name, age, registerTime: 'YYYY-MM-DD' }

// 数据适配器:将原格式转换为目标格式
function userDataAdapter(rawData) {
  return {
    id: rawData.user_id, // 字段名转换
    name: rawData.user_name,
    age: rawData.user_age,
    // 数据格式转换(时间戳→日期字符串)
    registerTime: new Date(rawData.reg_time).toLocaleDateString(),
  };
}

// 使用适配器后,组件拿到符合预期的数据
const user = userDataAdapter(rawUser);
console.log(user);
// {
//   id: 1001,
//   name: '张三',
//   age: 25,
//   registerTime: '2021-05-03'
// }

// 后端接口变化时,只需修改适配器,无需修改所有使用数据的组件;
// 组件只需关注自己需要的格式,不用处理各种兼容逻辑

2. 方法适配器:转换方法接口 当两个方法的参数、返回值或调用方式不兼容时,用适配器包装方法,统一接口。

js 复制代码
// 旧系统的接口(回调方式)
function oldFetchData(url, successCallback, errorCallback) {
  setTimeout(() => {
    if (url.includes('success')) {
      successCallback({ data: '模拟数据' });
    } else {
      errorCallback(new Error('请求失败'));
    }
  }, 1000);
}

// 新系统需要的接口(Promise 方式)
// 目标:fetchData(url) → 返回 Promise

// 方法适配器:将回调接口转换为 Promise 接口
function fetchDataAdapter(url) {
  return new Promise((resolve, reject) => {
    // 调用旧接口,用适配器转换回调为 Promise 的 resolve/reject
    oldFetchData(
      url,
      (data) => resolve(data), // 成功回调→resolve
      (err) => reject(err), // 失败回调→reject
    );
  });
}

// 使用:新系统用 Promise 方式调用,无需关心旧接口的回调逻辑
fetchDataAdapter('/api/success')
  .then((data) => console.log('成功:', data))
  .catch((err) => console.log('失败:', err));

// 新代码可以用现代方式(如 async/await)调用旧接口,无需兼容回调写法;
// 旧接口逻辑不变,避免修改可能带来的风险。

** 3. 第三方库适配:统一组件接口** 引入第三方库时,其 API 可能与项目现有组件的接口不一致,用适配器统一调用方式。

js 复制代码
// 第三方日历组件 A 的接口(返回 { year, month, day })
function CalendarA() {
  this.getSelectedDate = () => ({ year: 2023, month: 10, day: 5 });
}

// 第三方日历组件 B 的接口(返回 '2023-10-05' 字符串)
function CalendarB() {
  this.getDate = () => '2023-10-05';
}

// 项目需要的统一接口:getSelected() → 返回 Date 对象
function calendarAdapter(calendar) {
  // 判断是哪种组件,返回统一接口
  if (calendar instanceof CalendarA) {
    return {
      getSelected: () => {
        const { year, month, day } = calendar.getSelectedDate();
        return new Date(year, month - 1, day); // 转换为 Date
      },
    };
  } else if (calendar instanceof CalendarB) {
    return {
      getSelected: () => new Date(calendar.getDate()), // 转换为 Date
    };
  }
}

// 使用:无论用哪种日历,都用统一的 getSelected() 接口
const calA = new CalendarA();
const adaptedA = calendarAdapter(calA);
console.log(adaptedA.getSelected()); // Date 对象

const calB = new CalendarB();
const adaptedB = calendarAdapter(calB);
console.log(adaptedB.getSelected()); // Date 对象

// 项目中所有使用日历的地方都调用统一接口,更换第三方库时只需修改适配器;
// 避免在业务代码中散落各种库的兼容逻辑。

适配器模式 vs 装饰器模式的核心区别 适配器模式关注 "接口兼容,不添加新功能,只做格式 / 接口转换" ,装饰器模式关注 "功能扩展,保留原接口,添加新功能"

使用适配器模式的注意事项

  • 不要过度使用:如果接口本身可以统一(如和后端约定好数据格式),优先统一接口,而非依赖适配器;
  • 明确适配目标:适配器的目标接口应清晰(如 "必须返回 Date 对象"),避免适配后仍有格式混乱;
  • 适配逻辑集中:将所有适配逻辑放在专门的适配器中,避免散落在业务代码里(如创建 api/adapters 目录统一管理)。

3. 代理模式(Proxy Pattern)

代理模式(Proxy Pattern)是一种通过创建 "代理对象" 来控制对原对象的访问的结构型模式。代理对象与原对象实现相同的接口,使用者无需区分两者,但代理可以在**"访问原对象的前后"插入额外逻辑**(如拦截读写、验证权限、缓存结果等),从而实现对原对象的保护或增强。

想象你通过 "中介" 租房:你不直接接触房东(原对象),而是通过中介(代理)完成看房、签约等操作。中介会先核实你的身份(权限验证)、筛选符合你需求的房源(过滤逻辑),再带你联系房东 ------ 这就是代理模式的直观类比:

  • 原对象(Subject):被代理的目标对象(如数据对象、组件、函数);
  • 代理对象(Proxy):包装原对象的中间层,对外暴露与原对象一致的接口;
  • 核心作用:在不修改原对象的前提下,通过代理控制访问过程,添加额外逻辑(如拦截、缓存、权限校验等)。

JavaScript 原生提供了 Proxy 对象,可直接创建代理,是实现代理模式的最佳工具。它允许你拦截对象的几乎所有操作(如属性读写、函数调用、删除属性等),并在拦截过程中注入自定义逻辑。

1. 基础用法:拦截对象的读写操作

js 复制代码
// 原对象:用户数据
const user = {
  name: '张三',
  age: 20,
};

// 代理对象:拦截 user 的属性读写
const userProxy = new Proxy(user, {
  // 拦截属性读取(target 是原对象,prop 是属性名)
  get(target, prop) {
    console.log(`读取属性 ${prop}`); // 额外逻辑:记录日志
    // 控制访问:如果读取的是 age,返回成年/未成年
    if (prop === 'age') {
      return target[prop] >= 18 ? '成年' : '未成年';
    }
    return target[prop]; // 正常返回原属性值
  },

  // 拦截属性修改(target 是原对象,prop 是属性名,value 是新值)
  set(target, prop, value) {
    console.log(`修改属性 ${prop} 为 ${value}`); // 额外逻辑:记录日志
    // 控制修改:age 不能小于 0
    if (prop === 'age' && value < 0) {
      console.error('年龄不能为负数');
      return false; // 阻止修改
    }
    target[prop] = value; // 正常修改原对象
    return true;
  },
});

// 使用代理(和使用原对象的方式完全一致)
console.log(userProxy.name); // 读取属性 name → 输出:张三
console.log(userProxy.age); // 读取属性 age → 输出:成年(被代理转换)

userProxy.age = -5; // 修改属性 age 为 -5 → 报错:年龄不能为负数(被代理阻止)
userProxy.age = 25; // 修改属性 age 为 25 → 成功

2. 数据校验与保护 通过代理拦截数据修改,确保数据符合规则(如表单验证、状态合法性检查)。

js 复制代码
// 原对象:表单数据
const formData = {
  username: '',
  password: '',
};

// 代理:验证表单数据
const formProxy = new Proxy(formData, {
  set(target, prop, value) {
    if (prop === 'username') {
      if (value.length < 3) {
        console.error('用户名至少3个字符');
        return false;
      }
    }
    if (prop === 'password') {
      if (value.length < 6) {
        console.error('密码至少6个字符');
        return false;
      }
    }
    target[prop] = value;
    return true;
  },
});

// 使用:不符合规则的修改会被拦截
formProxy.username = '张'; // 报错:用户名至少3个字符
formProxy.password = '123'; // 报错:密码至少6个字符

3. 缓存代理:优化重复计算 / 请求

对耗时操作(如接口请求、复杂计算)的结果进行缓存,避免重复执行。

js 复制代码
// 原函数:模拟耗时的接口请求
async function fetchUser(id) {
  console.log(`发起请求:获取用户 ${id}`);
  // 模拟网络延迟
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return { id, name: `用户${id}` };
}

// 缓存代理:缓存请求结果
const fetchUserProxy = (() => {
  const cache = {}; // 缓存容器
  return async function (id) {
    // 如果缓存中有,直接返回
    if (cache[id]) {
      console.log(`使用缓存:用户 ${id}`);
      return cache[id];
    }
    // 否则调用原函数,并缓存结果
    const result = await fetchUser(id);
    cache[id] = result;
    return result;
  };
})();

// 使用:第一次请求走原函数,第二次走缓存
fetchUserProxy(1).then(console.log); // 发起请求 → { id:1, ... }
fetchUserProxy(1).then(console.log); // 使用缓存 → { id:1, ... }(无延迟)

4. 权限控制:限制对敏感资源的访问 通过代理拦截敏感操作(如文件下载、数据删除),确保只有授权用户才能执行。

js 复制代码
// 原对象:敏感操作(如删除数据)
const dataManager = {
  deleteData: (id) => console.log(`删除数据 ${id}`),
};

// 代理:验证权限后才允许删除
const dataManagerProxy = new Proxy(dataManager, {
  apply(target, thisArg, args) {
    // 拦截函数调用
    const userRole = 'guest'; // 假设当前用户是访客
    if (userRole !== 'admin') {
      console.error('权限不足:只有管理员可删除数据');
      return;
    }
    // 权限通过,调用原方法
    return target.deleteData(...args);
  },
});

// 使用:访客删除会被拦截
dataManagerProxy.deleteData(100); // 报错:权限不足

4. Vue 响应式原理的核心 Vue 2 的 Object.defineProperty 和 Vue 3 的 Proxy 本质是代理模式:通过拦截数据的读写,在数据变化时自动触发视图更新(依赖收集与派发更新)。

js 复制代码
// Vue 响应式简化原理(基于 Proxy)
function reactive(obj) {
  return new Proxy(obj, {
    get(target, prop) {
      // 收集依赖(记录哪个组件使用了该属性)
      track(target, prop);
      return target[prop];
    },
    set(target, prop, value) {
      target[prop] = value;
      // 派发更新(通知依赖的组件重新渲染)
      trigger(target, prop);
      return true;
    },
  });
}

4. 组合模式(Composite Pattern) - 树形结构

组合模式(Composite Pattern),核心是将 "单个对象" 和 "对象集合" 统一视为同一类对象(组件),通过**"树形结构"组合它们,并提供一致的操作接口**。简单说,就是 "用一棵树表示整体与部分的关系,让用户对单个对象和组合对象的操作完全一致"。

前端开发中,很多场景存在 "整体 - 部分" 的树形结构:

  • 菜单(一级菜单包含二级菜单,二级菜单可能包含菜单项);
  • 文件夹(文件夹包含子文件夹和文件);
  • 富文本编辑器(文档包含段落,段落包含文字、图片等元素);
  • 组件树(如 React/Vue 组件的嵌套结构)。

这些场景的痛点是:单个对象(如菜单项)和组合对象(如菜单组)需要被统一处理(如统一渲染、统一触发事件)。如果分别处理,会导致代码冗余(重复判断 "是单个还是组合")。 组合模式通过 "树形结构 + 统一接口" 解决这个问题,让用户无需区分 "单个" 和 "组合",直接用同一套代码操作。

组合模式包含三个核心角色(前端实现会简化):

  • 抽象组件(Component):定义单个对象和组合对象的统一接口(如 render()、onClick(),表现为一个抽象类或对象,声明通用方法
  • 叶子节点(Leaf):树形结构中的 "单个对象"(无子节点),实现抽象组件的接口,表现为一个菜单项、文件、单个组件
  • 组合节点(Composite):树形结构中的 "组合对象"(包含子节点),实现抽象组件的接口,同时管理子节点,表现为一个菜单组、文件夹、组件树

以 "多级菜单" 为例,演示组合模式如何统一处理单个菜单项和菜单组:

1.基础实现(统一接口 + 树形结构)

js 复制代码
// 1. 抽象组件:定义统一接口(渲染、点击)
class MenuComponent {
  // 渲染自身(叶子和组合都需实现)
  render() {
    throw new Error('子类必须实现render方法');
  }

  // 点击事件(叶子和组合都需实现)
  onClick() {
    throw new Error('子类必须实现onClick方法');
  }
}

// 2. 叶子节点:单个菜单项(无子类)
class MenuItem extends MenuComponent {
  constructor(name, action) {
    super();
    this.name = name; // 菜单项名称
    this.action = action; // 点击后执行的动作
  }

  // 渲染为单个列表项
  render() {
    return `<li class="menu-item">${this.name}</li>`;
  }

  // 点击触发自身动作
  onClick() {
    console.log(`点击菜单项:${this.name}`);
    this.action?.(); // 执行预设动作
  }
}

// 3. 组合节点:菜单组(包含子节点)
class MenuGroup extends MenuComponent {
  constructor(name) {
    super();
    this.name = name; // 菜单组名称
    this.children = []; // 存储子节点(可以是MenuItem或MenuGroup)
  }

  // 添加子节点(支持嵌套)
  add(child) {
    this.children.push(child);
    // 返回当前对象,支持链式调用
    return this;
  }

  // 渲染为包含子节点的列表
  render() {
    // 递归渲染所有子节点
    const childrenHtml = this.children.map((child) => child.render()).join('');
    return `
      <li class="menu-group">
        <span>${this.name}</span>
        <ul>${childrenHtml}</ul>
      </li>
    `;
  }

  // 点击触发自身逻辑 + 递归触发子节点点击
  onClick() {
    console.log(`点击菜单组:${this.name}`);
    // 递归调用所有子节点的onClick(组合节点的特性)
    this.children.forEach((child) => child.onClick());
  }
}

2. 使用组合模式

js 复制代码
// 创建树形菜单结构
const rootMenu = new MenuGroup('文件');

// 给根菜单添加子节点(菜单项和子菜单组)
rootMenu.add(new MenuItem('新建', () => console.log('执行新建')));
rootMenu.add(new MenuItem('保存', () => console.log('执行保存')));

// 创建子菜单组"导出"
const exportSubMenu = new MenuGroup('导出');
exportSubMenu.add(new MenuItem('导出为PDF', () => console.log('导出PDF')));
exportSubMenu.add(new MenuItem('导出为图片', () => console.log('导出图片')));

// 将子菜单组添加到根菜单
rootMenu.add(exportSubMenu);

// 1. 统一渲染整个菜单树(无需区分叶子和组合)
const menuHtml = `<ul class="menu-root">${rootMenu.render()}</ul>`;
document.body.innerHTML = menuHtml;
// 渲染结果是嵌套的树形HTML:
// <ul class="menu-root">
//   <li class="menu-group">
//     <span>文件</span>
//     <ul>
//       <li class="menu-item">新建</li>
//       <li class="menu-item">保存</li>
//       <li class="menu-group">...</li> <!-- 导出子菜单 -->
//     </ul>
//   </li>
// </ul>

// 2. 统一触发点击事件(叶子和组合都响应)
rootMenu.onClick();
// 输出:
// 点击菜单组:文件
// 点击菜单项:新建 → 执行新建
// 点击菜单项:保存 → 执行保存
// 点击菜单组:导出
// 点击菜单项:导出为PDF → 导出PDF
// 点击菜单项:导出为图片 → 导出图片

组合模式的核心优势

  • 统一接口,简化操作:用户无需判断操作的是 "单个对象" 还是 "组合对象",用同一套方法(如 render()、onClick())处理即可,减少冗余代码。
  • 灵活扩展树形结构:可以随时给组合节点添加 / 删除子节点(如给菜单组新增菜单项),整个结构的操作逻辑不受影响。
  • 递归处理树形数据:组合节点通过递归调用子节点的方法(如 render() 中递归渲染子节点),天然适配树形结构的处理需求。

使用组合模式的注意事项

  • 区分 "叶子" 和 "组合" 的职责:叶子节点只处理自身逻辑,组合节点除了自身逻辑,还要管理子节点(add/remove)和递归调用子节点方法。
  • 避免过度设计:如果结构不是树形(如线性列表),或单个对象与组合对象的操作差异极大,无需使用组合模式。
  • 递归深度控制:过深的树形结构(如 10 层以上嵌套)可能导致递归性能问题(如渲染耗时),需合理控制层级或优化递归逻辑。

5. 外观模式(Facade Pattern) - 遥控器

外观模式(Facade Pattern)是一种结构型模式,核心是为复杂系统或一系列子系统提供一个简化的 "入口接口",隐藏内部的复杂性,让使用者无需关心系统的具体实现细节,只需通过这个统一入口就能完成操作。

简单说,外观模式就像 "遥控器":你无需知道电视内部的电路板、芯片如何工作,只需按遥控器上的 "开机""换台" 按钮(简化接口),就能控制电视 ------ 遥控器就是电视系统的 "外观"。

前端开发中,很多场景涉及 "多个关联操作" 或 "复杂系统交互":

  • 初始化一个页面可能需要依次调用接口请求、DOM 渲染、事件绑定、数据格式化等多个步骤;
  • 使用第三方库时,可能需要调用多个 API 才能完成一个简单功能(如图表库需要先初始化、设置数据、配置样式、渲染);
  • 跨模块协作时,一个功能可能需要调用多个模块的方法(如 "用户登录" 需 要调用表单验证模块、API 模块、权限模块)。 如果让使用者手动调用这些步骤,会导致:

如果让使用者手动调用这些步骤,会导致:

  • 代码冗余(每个使用处都要重复写一串步骤);
  • 耦合度高(使用者必须了解所有子系统的细节);
  • 维护困难(修改子系统时,所有使用处都要同步修改)。

外观模式通过 "封装复杂流程,提供简化接口" 解决这些问题,让使用者用最少的代码完成操作。

1. 简化复杂初始化流程

场景:页面加载时需要依次执行 "请求数据→格式化数据→渲染列表→绑定事件",用外观模式封装这些步骤。

js 复制代码
// 子系统1:请求数据
const DataService = {
  fetch: (url) => fetch(url).then((res) => res.json()),
};

// 子系统2:格式化数据
const DataFormatter = {
  format: (rawData) =>
    rawData.map((item) => ({
      id: item.id,
      name: item.user_name,
      status: item.status === 1 ? '活跃' : '禁用',
    })),
};

// 子系统3:渲染列表
const ListRenderer = {
  render: (data) => {
    const list = document.getElementById('list');
    list.innerHTML = data
      .map(
        (item) => `
      <li>
        <span>${item.name}</span>
        <span>${item.status}</span>
      </li>
    `,
      )
      .join('');
  },
};

// 子系统4:绑定事件
const EventBinder = {
  bind: () => {
    document.getElementById('list').addEventListener('click', (e) => {
      if (e.target.tagName === 'LI') {
        console.log('点击了行:', e.target.textContent);
      }
    });
  },
};

// 外观模式:提供简化的入口(封装所有子系统调用)
const ListFacade = {
  // 一键初始化列表
  init: async (url) => {
    try {
      const rawData = await DataService.fetch(url); // 调用子系统1
      const formattedData = DataFormatter.format(rawData); // 调用子系统2
      ListRenderer.render(formattedData); // 调用子系统3
      EventBinder.bind(); // 调用子系统4
      console.log('列表初始化完成');
    } catch (err) {
      console.error('初始化失败:', err);
    }
  },
};

// 使用:只需调用外观的init方法,无需关心内部步骤
ListFacade.init('https://api.example.com/users');

// 使用者无需知道 DataService、ListRenderer 等子系统的存在,只需调用 ListFacade.init();
// 若后续修改初始化流程(如新增 "数据缓存" 步骤),只需修改 ListFacade,所有使用处无需变动。

2. 简化第三方库使用

场景:使用复杂的图表库时,封装一个外观接口,简化 "创建图表" 的操作。

js 复制代码
// 第三方图表库(复杂,需要多步操作)
const ComplexChartLib = {
  create: (domId) => ({ id: domId, data: [], options: {} }),
  setData: (chart, data) => {
    chart.data = data;
  },
  setOptions: (chart, options) => {
    chart.options = options;
  },
  render: (chart) => {
    console.log(`渲染图表 ${chart.id}`, chart);
  },
};

// 外观模式:简化图表创建
const ChartFacade = {
  // 一键创建图表(封装所有步骤)
  createChart: (domId, data, options) => {
    const chart = ComplexChartLib.create(domId); // 步骤1:创建实例
    ComplexChartLib.setData(chart, data); // 步骤2:设置数据
    ComplexChartLib.setOptions(chart, {
      // 步骤3:设置默认+自定义配置
      title: '默认标题',
      ...options,
    });
    ComplexChartLib.render(chart); // 步骤4:渲染
    return chart;
  },
};

// 使用:无需了解第三方库的细节,直接传参即可
ChartFacade.createChart('my-chart', [10, 20, 30], { title: '用户增长' });
// 屏蔽了第三方库的复杂 API,降低学习成本;
// 若未来更换图表库,只需修改 ChartFacade 的实现,业务代码无需改动。

3. 统一模块交互接口

场景:用户登录需要调用 "验证、请求、权限" 三个模块,用外观模式统一入口

js 复制代码
// 子模块1:表单验证
const Validator = { check: (user) => user.name && user.pwd };

// 子模块2:登录请求
const LoginAPI = { request: (user) => fetch('/login', { method: 'POST', body: user }) };

// 子模块3:权限设置
const Auth = {
  setRole: (role) => {
    localStorage.setItem('role', role);
  },
};

// 外观模式:统一登录流程
const LoginFacade = {
  login: async (user) => {
    if (!Validator.check(user)) {
      // 调用验证模块
      alert('请输入用户名和密码');
      return false;
    }
    const res = await LoginAPI.request(user); // 调用请求模块
    const { role } = await res.json();
    Auth.setRole(role); // 调用权限模块
    return true;
  },
};

// 使用:只需调用login方法
LoginFacade.login({ name: 'admin', pwd: '123' });

外观模式的核心优势

  • 简化接口:将复杂流程封装为一个简单接口,降低使用难度(如用 init() 替代 4 个步骤调用);
  • 隔离变化:子系统的修改(如换图表库、改 API 地址)只需修改外观,不影响使用者;
  • 减少耦合:使用者无需依赖多个子系统,只需依赖外观,降低代码间的关联。

外观模式和适配器模式:核心区别

  • 外观模式【简化器】:为复杂系统提供 "简化入口",不改变子系统接口,仅封装流程,让使用更简单
  • 适配器模式【转换器】:适配器模式 解决 "接口不兼容" 问题 改变接口格式,让不兼容的对象可以协作

前端外观模式的典型应用场景

  • 复杂初始化流程:如页面加载、组件初始化等需要多步操作的场景;
  • 第三方库封装:如对 ECharts、Three.js 等复杂库封装简化接口;
  • 跨模块协作:如登录、支付等需要多个模块配合的功能;
  • 工具类封装:如将 localStorage 的 getItem/setItem 封装为更易用storage.get()/storage.set()。

使用外观模式的注意事项:

  • 不要过度封装:简单的操作(如单个函数调用)无需封装,避免增加不必要的层级;
  • 保持外观的单一职责:一个外观接口只负责一个功能(如 LoginFacade 只处理登录),避免成为 "万能接口";
  • 不限制直接使用子系统:外观是 "简化入口",但不应禁止使用者直接调用子系统(灵活度更高)。

行为型模式(Behavioral Pattern)

行为型模式(Behavioral Patterns),核心解决 "对象之间的交互行为" 问题 ------ 即如何通过合理的方式协调多个对象的通信、职责分配和行为协作,让系统的行为更清晰、灵活、可复用。

简单说,创建型模式关注 "如何创建对象"结构型模式关注 "对象如何组合" ,而行为型模式关注 "对象如何交互",目的是通过优化对象间的行为协作,提升系统的可维护性和扩展性。

前端开发中,行为型模式多与组件通信、事件处理、状态流转相关,以下是最常用的几种:

  • 观察者 / 发布-订阅模式:解决 "一对多" 通信问题(如组件通知、事件总线);
  • 策略模式:让算法 / 行为可替换(如表单验证、动态渲染);
  • 状态模式:封装状态依赖的行为(如组件状态切换);
  • 命令模式:解耦请求发送者和接收者(如撤销功能、批量操作)。

行为型模式的核心价值是通过规范对象间的交互方式,让系统的行为更清晰、更易于扩展 ------ 毕竟,多个对象协作时,"如何配合" 往往比 "各自做什么" 更重要。

1. 观察者模式 - 数组存储观察者或者函数

观察者模式(Observer Pattern)核心思想是 "定义一对多的依赖关系:当一个对象(被观察者)的状态发生变化时,所有依赖它的对象(观察者)会自动收到通知并更新"。

简单说,它就像 "公众号订阅":

  • 公众号(被观察者):只有一个,负责发布内容;
  • 订阅者(观察者):可以有多个,订阅后会收到公众号的所有推送;
  • 核心逻辑:公众号无需知道订阅者是谁,只要状态变化(发新文章),所有订阅者都会被通知。

前端开发中,"一个状态变化需要联动多个地方更新" 的场景随处可见:

  • 父组件状态更新,需要通知多个子组件重新渲染;
  • 全局主题切换(如深色 / 浅色模式),需要所有页面组件同步样式;
  • 接口请求完成后,需要通知列表组件刷新、弹窗组件关闭、日志组件记录。

如果不用观察者模式,会导致:

  • 代码耦合严重(被观察者需要手动调用每个观察者的更新方法);
  • 扩展性差(新增观察者时,需要修改被观察者的代码);
  • 维护困难(联动逻辑散落在各个地方)。 观察者模式通过 "解耦被观察者和观察者" 解决这些问题,让联动逻辑更清晰、更灵活。

观察者模式的核心结构

观察者模式有两个核心角色,以及一套标准交互流程:

  • 被观察者(Subject):维护观察者列表,提供 "订阅" 和 "通知" 接口;核心方法是subscribe()(添加观察者)、notify()(通知所有观察者)
  • 观察者(Observer):定义接收通知后的更新逻辑;update()(接收通知并执行更新)

交互流程:

  • 观察者通过被观察者的 subscribe() 方法订阅;
  • 被观察者状态变化时,调用 notify() 方法;
  • notify() 遍历所有观察者,调用其 update() 方法传递最新状态;
  • 观察者通过 update() 方法接收状态,执行自身更新逻辑。

前端观察者模式的典型应用

以 "全局主题切换" 为例,实现观察者模式: 1. 基础实现(标准结构)

js 复制代码
// 1. 被观察者:主题管理器(管理主题状态和观察者)
class ThemeSubject {
  constructor() {
    this.observers = []; // 存储所有观察者
    this.currentTheme = 'light'; // 初始状态:浅色模式
  }

  // 订阅:添加观察者
  subscribe(observer) {
    // 避免重复订阅
    if (!this.observers.includes(observer)) {
      this.observers.push(observer);
    }
  }

  // 取消订阅:移除观察者
  unsubscribe(observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }

  // 通知所有观察者(状态变化时调用)
  notify() {
    this.observers.forEach((observer) => {
      // 传递最新状态给观察者
      observer.update(this.currentTheme);
    });
  }

  // 修改主题(触发状态变化)
  setTheme(theme) {
    if (this.currentTheme !== theme) {
      this.currentTheme = theme;
      this.notify(); // 状态变化,通知观察者
    }
  }
}

// 2. 观察者1:页面标题组件(接收通知后更新标题样式)
class TitleObserver {
  update(theme) {
    console.log(`标题组件:切换到${theme}模式`);
    document.title = theme === 'dark' ? '【深色模式】我的应用' : '我的应用';
  }
}

// 3. 观察者2:页面样式组件(接收通知后更新body样式)
class StyleObserver {
  update(theme) {
    console.log(`样式组件:切换到${theme}模式`);
    document.body.className = theme;
  }
}

// 4. 使用观察者模式
const themeSubject = new ThemeSubject();
const titleObserver = new TitleObserver();
const styleObserver = new StyleObserver();

// 订阅
themeSubject.subscribe(titleObserver);
themeSubject.subscribe(styleObserver);

// 切换主题(触发通知)
themeSubject.setTheme('dark');
// 输出:
// 标题组件:切换到dark模式
// 样式组件:切换到dark模式

// 取消订阅(标题组件不再接收通知)
themeSubject.unsubscribe(titleObserver);
themeSubject.setTheme('light');
// 输出:
// 样式组件:切换到light模式

2. 简化实现(函数 + 对象)

前端开发中,很少用 class 定义观察者(过于繁琐),更常用 "函数 + 对象" 简化实现,核心逻辑不变:

js 复制代码
// 被观察者:简化版主题管理器
const themeManager = {
  observers: [],
  currentTheme: 'light',

  // 订阅:直接接收回调函数作为观察者
  on(callback) {
    this.observers.push(callback);
  },

  // 取消订阅
  off(callback) {
    this.observers = this.observers.filter((cb) => cb !== callback);
  },

  // 通知
  emit() {
    this.observers.forEach((callback) => callback(this.currentTheme));
  },

  setTheme(theme) {
    if (this.currentTheme !== theme) {
      this.currentTheme = theme;
      this.emit();
    }
  },
};

// 观察者1:标题更新函数
function updateTitle(theme) {
  document.title = theme === 'dark' ? '【深色模式】我的应用' : '我的应用';
}

// 观察者2:样式更新函数
function updateStyle(theme) {
  document.body.className = theme;
}

// 订阅
themeManager.on(updateTitle);
themeManager.on(updateStyle);

// 切换主题
themeManager.setTheme('dark'); // 触发两个函数执行

前端观察者模式的典型应用场景

  • 组件通信:父子组件(父组件是被观察者,子组件是观察者)、兄弟组件(通过共同的被观察者通信);
  • 状态联动:如表单输入变化联动其他表单项、全局状态(如用户登录状态)变化联动页面组件;
  • 异步事件通知:接口请求完成后,通知多个组件更新数据(如列表组件刷新、加载状态关闭);
  • 框架核心机制:Vue 的响应式(数据是被观察者,DOM 是观察者)、React 的 setState(状态是被观察者,组件是观察者)。

使用观察者模式的注意事项:

  • 避免内存泄漏:观察者不再使用时,必须取消订阅(如 React 组件卸载时,移除 EventBus 订阅),否则被观察者会一直持有观察者引用,导致内存泄漏;
js 复制代码
// React 组件示例:卸载时取消订阅
useEffect(() => {
  themeManager.on(updateStyle);
  // 卸载时取消订阅
  return () => themeManager.off(updateStyle);
}, []);
  • 控制通知频率:频繁触发的状态变化(如输入框实时输入),需防抖 / 节流,避免观察者频繁更新导致性能问题;
  • 避免循环依赖:观察者更新时又触发被观察者状态变化,可能导致无限循环(如 update 中调用 setTheme)。

观察者模式的核心是 "状态变化自动通知依赖者",它解耦了被观察者和观察者,它的价值不在于 "复杂的代码结构",而在于 "清晰的交互逻辑"------ 让 "一个变化带动多个变化" 的场景,从 "手动调用一堆方法" 变成 "自动通知",大幅降低代码耦合度。

2. 发布-订阅模式 - 对象存储事件和函数数组的映射

发布 - 订阅模式(Publish-Subscribe Pattern,简称 Pub/Sub)是观察者模式的进阶版,核心思想是通过一个 "中间事件中心" 解耦发布者(Publishers)和订阅者(Subscribers)------ 发布者无需知道任何订阅者的存在,订阅者也无需知道发布者是谁,两者通过事件中心完成通信:发布者向中心发布事件,订阅者向中心订阅事件,事件中心负责将事件通知给所有相关订阅者。

简单说,它就像 "报纸订阅系统":

  • 订阅者(读者):向报社(事件中心)订阅报纸,留下接收地址;
  • 发布者(记者):写完文章后交给报社,无需关心谁会读;
  • 事件中心(报社):收到文章后,按订阅名单将报纸送到所有读者手中。

观察者模式虽然能解决 "一对多" 联动,但存在一个痛点:被观察者和观察者必须直接关联(被观察者持有观察者列表),导致耦合度较高(比如父子组件通信可行,但跨模块、跨页面通信就很麻烦)。 前端开发中,"跨组件 / 跨模块通信" 是高频需求:

  • 页面 A 的登录按钮点击后,需要通知页面 B 显示用户信息、页面 C 更新导航栏、接口模块保存 Token;
  • 第三方插件触发某个事件后,需要通知项目中的多个业务模块响应。

发布 - 订阅模式通过 "事件中心" 彻底解耦发布者和订阅者,让它们无需直接关联,完美解决跨场景通信问题。

发布 - 订阅模式的核心结构

相比观察者模式,发布 - 订阅模式多了 "事件中心" 角色,核心结构更清晰:

  • 发布者(Publisher):向事件中心发布事件(无需关心订阅者);核心方法是 publish(event, data)(发布事件,携带数据)
  • 订阅者(Subscriber):向事件中心订阅事件(无需关心发布者);核心方法是 subscribe(event, callback)(订阅事件,注册回调)
  • 事件中心(Event Center):管理事件 - 订阅者映射,转发事件通知;核心方法是存储事件回调列表、匹配发布 / 订阅、触发回调

交互流程:

  • 订阅者通过事件中心的 subscribe() 订阅事件,注册回调函数;
  • 发布者通过事件中心的 publish() 发布事件,携带相关数据;
  • 事件中心找到该事件对应的所有回调函数,依次执行(传递数据);
  • 订阅者通过回调函数接收数据,执行自身业务逻辑。

前端发布 - 订阅模式的典型应用

前端最常用的实现是 "全局事件总线(EventBus)",以下是完整示例:

1. 基础实现(通用 EventBus)

js 复制代码
// 事件中心:全局 EventBus
const EventBus = {
  // 存储事件-回调映射:key=事件名,value=回调数组
  events: {},

  // 1. 订阅事件:注册回调
  subscribe(eventName, callback) {
    // 若事件不存在,初始化回调数组
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    // 避免重复订阅同一回调
    if (!this.events[eventName].includes(callback)) {
      this.events[eventName].push(callback);
    }
  },

  // 2. 发布事件:触发所有订阅者的回调
  publish(eventName, data) {
    // 若事件无订阅者,直接返回
    if (!this.events[eventName]) return;
    // 遍历所有回调,传递数据并执行
    this.events[eventName].forEach((callback) => {
      callback(data);
    });
  },

  // 3. 取消订阅:移除回调
  unsubscribe(eventName, callback) {
    if (!this.events[eventName]) return;
    // 过滤掉要移除的回调
    this.events[eventName] = this.events[eventName].filter((cb) => cb !== callback);
    // 若事件无回调,可删除该事件(优化内存)
    if (this.events[eventName].length === 0) {
      delete this.events[eventName];
    }
  },

  // 4. 清空事件:移除某事件的所有订阅者
  clear(eventName) {
    if (eventName) {
      delete this.events[eventName];
    } else {
      // 无参数则清空所有事件
      this.events = {};
    }
  },
};

2. 简化实现(函数 + 对象)

js 复制代码
// 场景1:订阅者1(导航栏组件)- 订阅登录成功事件
function handleLoginSuccess(user) {
  console.log('导航栏:更新用户信息', user.name);
  document.getElementById('nav-user').textContent = user.name;
}
EventBus.subscribe('loginSuccess', handleLoginSuccess);

// 场景2:订阅者2(个人中心组件)- 订阅登录成功事件
EventBus.subscribe('loginSuccess', (user) => {
  console.log('个人中心:加载用户数据', user.id);
  // 模拟请求个人数据
  fetch(`/api/user/${user.id}`).then((res) => res.json());
});

// 场景3:发布者(登录组件)- 发布登录成功事件
document.getElementById('login-btn').addEventListener('click', () => {
  // 模拟登录请求成功
  const user = { id: 1001, name: '张三', role: 'admin' };
  console.log('登录组件:发布登录成功事件');
  EventBus.publish('loginSuccess', user); // 发布事件,携带用户数据
});

// 场景4:取消订阅(组件卸载时)
// 假设个人中心组件卸载,不再接收事件
EventBus.unsubscribe('loginSuccess', handleLoginSuccess);

// 场景5:清空事件(退出登录时)
document.getElementById('logout-btn').addEventListener('click', () => {
  EventBus.clear('loginSuccess');
});

// 点击登录按钮,触发loginSuccess事件
// 登录组件:发布登录成功事件
// 导航栏:更新用户信息 张三
// 个人中心:加载用户数据 1001

3. 进阶:带命名空间的 EventBus(避免事件名冲突)

如果项目较大,不同模块可能用相同的事件名(如 update),导致冲突。可以给 EventBus 增加 "命名空间" 功能:

js 复制代码
const NamespacedEventBus = {
  events: {},

  // 订阅:支持命名空间,如 'user:login'、'order:update'
  subscribe(eventName, callback) {
    const [namespace, event] = eventName.split(':');
    if (!namespace || !event) {
      throw new Error('事件名必须带命名空间,格式:namespace:event');
    }
    if (!this.events[namespace]) this.events[namespace] = {};
    if (!this.events[namespace][event]) this.events[namespace][event] = [];
    this.events[namespace][event].push(callback);
  },

  // 发布:对应命名空间的事件
  publish(eventName, data) {
    const [namespace, event] = eventName.split(':');
    if (!this.events[namespace]?.[event]) return;
    this.events[namespace][event].forEach((cb) => cb(data));
  },

  // 取消订阅:可清空整个命名空间
  unsubscribe(eventName, callback) {
    const [namespace, event] = eventName.split(':');
    if (!this.events[namespace]) return;
    if (!event) {
      delete this.events[namespace]; // 清空命名空间
      return;
    }
    if (callback) {
      this.events[namespace][event] = this.events[namespace][event].filter((cb) => cb !== callback);
    } else {
      delete this.events[namespace][event]; // 清空该事件
    }
  },
};

// 使用:带命名空间,避免冲突
NamespacedEventBus.subscribe('user:login', (user) => console.log('用户登录:', user));
NamespacedEventBus.subscribe('order:update', (order) => console.log('订单更新:', order));
NamespacedEventBus.publish('user:login', { id: 1 }); // 只触发用户登录的订阅者

前端发布 - 订阅模式的典型应用场景

  • 跨组件 / 跨页面通信:如登录成功后通知所有相关组件更新状态、购物车数量变化后通知导航栏和订单页;
  • 全局状态联动:如全局主题切换、语言切换,通知所有页面组件同步更新;
  • 异步事件通知:如文件上传完成后,通知进度条组件隐藏、列表组件刷新、通知组件提示;
  • 框架 / 库的事件机制:如 Vue 的 <math xmlns="http://www.w3.org/1998/Math/MathML"> e m i t / emit/ </math>emit/on(虽然是组件级,但核心是 Pub/Sub 思想)、Node.js 的 EventEmitter、Redux 的 subscribe。

使用发布 - 订阅模式的注意事项:

  • 避免事件名冲突:项目中统一事件名规范(如带命名空间 user:login、order:create),或用常量定义事件名;
  • 防止内存泄漏:订阅者不再使用时(如组件卸载),必须取消订阅,否则事件中心会一直持有回调引用,导致内存泄漏;
js 复制代码
// React 组件示例:卸载时取消订阅
useEffect(() => {
  const callback = (user) => console.log('用户登录', user);
  EventBus.subscribe('loginSuccess', callback);
  // 卸载时取消订阅
  return () => EventBus.unsubscribe('loginSuccess', callback);
}, []);
  • 控制事件频率:高频触发的事件(如输入框实时搜索),需给回调函数加防抖 / 节流,避免性能问题;
  • 避免过度使用:不要把所有通信都用 Pub/Sub,简单的父子组件通信用 props / 回调更清晰,Pub/Sub 适合跨场景的 "全局通信"。
js 复制代码
// React 组件示例:使用防抖
useEffect(() => {
  const debouncedCallback = debounce((user) => console.log('用户登录', user), 300);
  EventBus.subscribe('loginSuccess', debouncedCallback);
  return () => EventBus.unsubscribe('loginSuccess', debouncedCallback);
}, []);

发布 - 订阅模式 vs 观察者模式(核心区别)

用表格清晰对比:

维度 发布 - 订阅模式 观察者模式
核心结构 发布者 → 事件中心 → 订阅者(三层) 被观察者 → 观察者(两层)
耦合度 极低(发布者和订阅者无直接关联) 较高(被观察者持有观察者列表)
通信范围 全局通信(跨组件、跨模块) 局部通信(组件内部、父子组件)
事件管理 事件中心统一管理(支持命名空间、清空) 被观察者各自管理自己的观察者
适用场景 全局状态通知、跨模块通信 组件内部状态联动、父子组件通信
前端示例 EventBus、Vuex/Redux 的通知机制 DOM 事件(addEventListener)、Vue 组件自定义事件

一句话总结:发布 - 订阅模式是 "解耦版" 的观察者模式,通过事件中心打破了发布者和订阅者的直接依赖,更适合复杂项目的全局通信。

发布 - 订阅模式的核心是 "事件中心解耦发布者和订阅者",它让跨组件、跨模块的通信变得简单灵活,是前端复杂项目中不可或缺的设计模式。

它的价值不在于 "代码多优雅",而在于 "解耦能力强"------ 让原本需要互相依赖的组件 / 模块,变成 "各自独立、通过事件中心协作" 的个体,大幅降低项目的维护成本。前端开发中,几乎所有复杂项目都会封装一个全局 EventBus,本质就是发布 - 订阅模式的实践。

3. 策略模式 - 映射替代if/else

策略模式(Strategy Pattern)核心是将 "变化的算法 / 行为" 与 "稳定的使用逻辑" 解耦 ------ 通过封装一系列可替换的策略(算法 / 行为),让使用者能根据场景动态选择策略,无需关心策略的内部实现。它不仅能替代冗长的 if-else/switch 逻辑,还能让代码更易扩展、复用和维护。

简单类比:你要去上班("使用算法的场景"),可以选择地铁、公交、自驾等不同方式("不同策略"),每种方式的实现细节(路线、耗时、成本)都不同,但最终目的都是到达公司("统一目标")。你可以根据天气、时间灵活切换方式,无需修改 "上班" 这个核心逻辑。

策略模式的核心结构

策略模式包含三个核心角色(前端实现会灵活简化,但思想不变):

角色 作用 前端实现形式
环境类(Context) 持有策略的引用,提供统一的策略调用接口;负责根据场景选择 / 切换策略。 函数、类或对象(如验证器、支付管理器)
策略接口(Strategy) 定义所有策略的统一规范(如方法名、参数格式),保证策略可替换。 隐式约定(如策略函数的参数 / 返回值一致)或抽象方法
具体策略(ConcreteStrategy) 实现策略接口,封装具体的算法 / 行为(如手机号验证、微信支付逻辑)。 独立函数、对象方法或类

核心交互流程:

  • 环境类初始化时,可指定默认策略;
  • 使用者通过环境类的接口,传入参数或指定策略类型;
  • 环境类选择对应的具体策略,调用其核心方法;
  • 环境类返回策略执行结果,使用者无需直接操作策略。

没有策略模式时,面对多算法场景,我们常写冗长的 if-else/switch 逻辑,带来三大问题:

  • 代码臃肿:多个算法混杂在一个函数中,逻辑混乱,可读性差;
  • 耦合度高:算法与使用逻辑绑定,修改一个算法可能影响其他逻辑;
  • 扩展性差:新增算法需修改原有代码(违反 "开闭原则"),容易引入 bug。
js 复制代码
// 臃肿的 if-else 逻辑:验证逻辑与选择逻辑混杂
function validate(value, type) {
  if (type === 'required') {
    return value.trim() !== '' ? null : '不能为空';
  } else if (type === 'phone') {
    const reg = /^1[3-9]\d{9}$/;
    return reg.test(value) ? null : '手机号格式错误';
  } else if (type === 'email') {
    const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return reg.test(value) ? null : '邮箱格式错误';
  } else if (type === 'minLength') {
    return value.length >= 6 ? null : '至少6个字符';
  }
}

// 使用:每次调用都要写冗长的条件判断
validate('13800138000', 'phone');
validate('test@example.com', 'email');

前端策略模式典型应用

前端开发中,策略模式无需严格遵循 "类结构",更常用函数 / 对象实现,以下是三种典型场景: 1. 基础实现:函数式策略(最常用) 适合简单场景(如表单验证、格式转换),用对象存储策略函数,环境类简化为 "策略调用器"。

  • 步骤 1:封装具体策略(独立函数)
js 复制代码
// 具体策略:封装不同的验证算法(统一约定:参数为value,返回错误信息或null)
const ValidatorStrategies = {
  // 非空验证
  required: (value) => (value.trim() !== '' ? null : '不能为空'),
  // 手机号验证
  phone: (value) => (/^1[3-9]\d{9}$/.test(value) ? null : '手机号格式错误'),
  // 邮箱验证
  email: (value) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : '邮箱格式错误'),
  // 最小长度验证(支持额外参数)
  minLength: (value, length = 6) => (value.length >= length ? null : `至少${length}个字符`),
  // 自定义正则验证(支持动态传入正则)
  regex: (value, reg) => (reg.test(value) ? null : '格式不符合要求'),
};

步骤 2:实现环境类(策略调用器)

js 复制代码
// 环境类:验证器(统一调用策略,处理策略选择)
class Validator {
  constructor() {
    this.rules = []; // 存储当前字段的验证规则(策略+参数)
  }

  // 添加验证规则(选择策略)
  add(value, strategyType, ...params) {
    // 找到对应的策略函数
    const strategy = ValidatorStrategies[strategyType];
    if (!strategy) throw new Error(`不存在${strategyType}策略`);

    // 封装"策略调用逻辑"到规则中
    this.rules.push(() => strategy(value, ...params));
  }

  // 执行所有验证(统一调用策略)
  validate() {
    // 遍历所有规则,执行策略
    for (const rule of this.rules) {
      const errorMsg = rule();
      if (errorMsg) return errorMsg; // 有错误直接返回
    }
    return null; // 无错误
  }
}

步骤 3:使用策略模式

js 复制代码
// 场景:验证表单字段
const username = 'zhangsan';
const phone = '13800138000';
const password = '12345';

// 验证用户名(非空 + 最小长度3)
const usernameValidator = new Validator();
usernameValidator.add(username, 'required');
usernameValidator.add(username, 'minLength', 3);
console.log(usernameValidator.validate()); // null(无错误)

// 验证手机号(非空 + 手机号格式)
const phoneValidator = new Validator();
phoneValidator.add(phone, 'required');
phoneValidator.add(phone, 'phone');
console.log(phoneValidator.validate()); // null

// 验证密码(非空 + 最小长度6)
const passwordValidator = new Validator();
passwordValidator.add(password, 'required');
passwordValidator.add(password, 'minLength', 6);
console.log(passwordValidator.validate()); // 至少6个字符(错误)

// 扩展:自定义正则验证(新增策略无需修改Validator)
ValidatorStrategies.idCard = (value) => (/^\d{18}$/.test(value) ? null : '身份证格式错误');
const idCardValidator = new Validator();
idCardValidator.add('110101199001011234', 'idCard');
console.log(idCardValidator.validate()); // null
  1. 进阶实现:对象化策略(复杂逻辑) 当策略需要维护状态或包含多个方法时,可将策略封装为对象(或类),适合复杂场景(如支付方式、动态渲染)。
js 复制代码
// 具体策略1:微信支付(含状态和多方法)
const WechatPayStrategy = {
  name: '微信支付',
  // 策略状态:支付状态
  status: 'idle',
  // 核心方法:发起支付
  pay(amount) {
    this.status = 'processing';
    console.log(`发起微信支付,金额:${amount}元`);
    // 模拟支付异步流程
    return new Promise((resolve) => {
      setTimeout(() => {
        this.status = 'success';
        resolve({ success: true, msg: '微信支付成功' });
      }, 1000);
    });
  },
  // 辅助方法:查询支付状态
  queryStatus() {
    return this.status;
  },
};

// 具体策略2:支付宝支付
const AlipayStrategy = {
  name: '支付宝支付',
  status: 'idle',
  pay(amount) {
    this.status = 'processing';
    console.log(`发起支付宝支付,金额:${amount}元`);
    return new Promise((resolve) => {
      setTimeout(() => {
        this.status = 'success';
        resolve({ success: true, msg: '支付宝支付成功' });
      }, 800);
    });
  },
  queryStatus() {
    return this.status;
  },
};

// 环境类:支付管理器(统一调度策略)
class PaymentManager {
  constructor(defaultStrategy) {
    this.currentStrategy = defaultStrategy; // 默认策略
  }

  // 切换策略(动态切换支付方式)
  setStrategy(strategy) {
    this.currentStrategy = strategy;
    console.log(`切换支付方式为:${strategy.name}`);
  }

  // 统一调用策略的支付方法
  async pay(amount) {
    if (!this.currentStrategy) throw new Error('请选择支付方式');
    return this.currentStrategy.pay(amount);
  }

  // 统一调用策略的辅助方法
  getPayStatus() {
    return this.currentStrategy.queryStatus();
  }
}

// 使用:动态切换支付策略
const paymentManager = new PaymentManager(WechatPayStrategy);

// 场景1:微信支付
paymentManager.pay(100).then(console.log); // 微信支付成功
console.log(paymentManager.getPayStatus()); // processing

// 场景2:切换为支付宝支付
paymentManager.setStrategy(AlipayStrategy);
paymentManager.pay(200).then(console.log); // 支付宝支付成功
  1. 简化实现:无环境类(直接映射策略) 对于极简单场景(如根据类型选择格式化逻辑),可省略环境类,直接通过 "策略映射表" 调用策略,代码更简洁。
js 复制代码
// 策略映射表:不同类型对应不同格式化策略
const FormatStrategies = {
  date: (value) => new Date(value).toLocaleDateString(),
  number: (value) => value.toFixed(2),
  upperCase: (value) => value.toUpperCase(),
};

// 直接调用策略(无环境类)
function formatValue(value, type) {
  const strategy = FormatStrategies[type];
  return strategy ? strategy(value) : value;
}

// 使用
console.log(formatValue(1620000000000, 'date')); // 2021-05-03
console.log(formatValue(123.456, 'number')); // 123.46
console.log(formatValue('hello', 'upperCase')); // HELLO

前端典型应用场景

  • 表单验证:不同字段(用户名、手机号、邮箱)的验证规则用不同策略;
  • 支付方式选择:微信、支付宝、银行卡支付等不同策略,支持动态切换;
  • 数据格式化:日期、数字、字符串的不同格式化逻辑(如后台返回的时间戳→本地日期);
  • 动态渲染:根据用户角色(管理员 / 普通用户)渲染不同界面元素(如按钮、菜单);
  • 排序算法:列表数据的升序、降序、按字段排序等策略;
  • 动画效果:组件入场动画的不同效果(淡入、滑入、缩放)。

策略模式的核心优势

  • 解耦逻辑:策略的 "实现" 与 "使用" 分离,修改策略不影响使用逻辑,新增策略无需修改原有代码(符合开闭原则);
  • 替代条件判断:用策略映射替代冗长的 if-else/switch,代码更简洁、可读性更高;
  • 灵活扩展:策略可独立新增、修改或删除,支持运行时动态切换(如支付方式、主题风格);
  • 复用性强:封装后的策略可在多个场景复用(如 "非空验证" 可用于表单所有字段);
  • 便于测试:每个策略独立封装,可单独编写单元测试,降低测试复杂度。

使用注意事项

  • 策略数量控制:如果策略数量过少(如仅 2 个),用 if-else 可能更简洁,无需过度设计;
  • 策略接口统一:所有策略必须遵循相同的接口规范(如参数、返回值一致),否则无法灵活替换;
  • 避免策略过度复杂:如果单个策略逻辑过于复杂(如包含大量状态和方法),可拆分策略或结合其他模式(如工厂模式创建策略);
  • 环境类职责单一:环境类仅负责策略的选择和调用,不应该包含策略的具体实现逻辑;
  • 前端灵活简化:无需严格遵循 "类结构",可根据场景用函数、对象等轻量形式实现(如 React/Vue 中常用函数策略)。

策略模式的本质是 "算法的封装与解耦",核心价值在于让代码更灵活、易维护、可扩展。前端开发中,它最适合处理 "存在多个相似算法,且需要动态选择" 的场景(如表单验证、支付方式),能有效替代臃肿的条件判断,同时提升代码复用率。 实现策略模式时,无需拘泥于 "类结构",可根据场景选择 "函数策略""对象策略" 或 "无环境类简化版"------ 核心是抓住 "策略独立封装、使用逻辑统一" 的思想,让代码在变化中保持清晰和稳定。

4. 状态模式

状态模式(State Pattern)核心思想是:将对象的状态封装为独立的 "状态类 / 对象",让对象的行为随状态变化而自动切换,同时将状态转换逻辑与对象本身解耦。简单说,就是 "状态决定行为,状态变化驱动行为变化",无需通过大量 if-else 判断状态来执行不同逻辑。

类比生活场景:电梯有 "待机""上升""下降""停止" 四种状态,每种状态下的行为不同(如 "待机" 时可响应呼叫,"上升" 时只响应更高楼层呼叫)。状态模式会将每种状态的行为封装起来,电梯只需切换状态,就能自动执行对应行为,无需在电梯类中写一堆状态判断逻辑。

状态模式的核心结构

角色 作用 前端实现形式
环境类(Context) 持有当前状态的引用,提供统一的外部接口;负责状态切换和行为委托。 组件实例、对象(如电梯、弹窗)
状态接口(State) 定义所有状态的统一行为规范(如 handle() 方法),保证状态可替换。 隐式约定(方法名 / 参数一致)或抽象对象
具体状态(ConcreteState) 实现状态接口,封装对应状态下的行为;可包含状态转换逻辑(如 "上升"→"停止")。 独立对象、函数或类

没有状态模式时,处理多状态对象会面临两大问题:

  • 状态判断臃肿:用 if-else/switch 判断对象状态,执行对应行为,代码冗长、可读性差(如电梯类中写 if(状态===上升) { ... } else if(状态===下降) { ... });
  • 耦合度高:状态转换逻辑和行为逻辑混杂在环境类中,修改一个状态的行为或转换规则,需改动环境类代码(违反 "开闭原则");
  • 扩展性差:新增状态时,需在环境类中添加新的条件判断,容易引入 bug。
js 复制代码
// 无状态模式:用 if-else 判断状态,行为与状态耦合
class Modal {
  constructor() {
    this.state = 'hidden'; // 初始状态:隐藏
  }

  // 行为依赖状态判断,臃肿且难维护
  toggle() {
    if (this.state === 'hidden') {
      console.log('弹窗显示');
      this.state = 'visible';
      document.body.classList.add('modal-open');
    } else if (this.state === 'visible') {
      console.log('弹窗隐藏');
      this.state = 'hidden';
      document.body.classList.remove('modal-open');
    } else if (this.state === 'loading') {
      console.log('加载中,无法切换');
    }
  }

  showLoading() {
    if (this.state === 'hidden' || this.state === 'visible') {
      console.log('显示加载中');
      this.state = 'loading';
    }
  }
}

前端状态模式典型应用

前端开发中,状态模式无需严格遵循 "类结构",可根据场景用 "对象""函数" 或 "组件状态" 实现,以下是三种典型场景:

1. 基础实现:对象化状态(最常用) 适合中小型场景(如弹窗、按钮状态管理),用对象封装每种状态的行为,环境类委托行为并管理状态切换。 步骤 1:封装具体状态(对象形式)

js 复制代码
// 具体状态1:隐藏状态(HiddenState)
const HiddenState = {
  // 状态名称(可选,便于调试)
  name: 'hidden',
  // 状态对应的行为1:切换显示/隐藏
  toggle(modal) {
    console.log('弹窗显示');
    // 触发状态转换:切换为显示状态
    modal.setState(modal.visibleState);
    document.body.classList.add('modal-open');
  },
  // 状态对应的行为2:显示加载
  showLoading(modal) {
    console.log('显示加载中');
    modal.setState(modal.loadingState);
  },
};

// 具体状态2:显示状态(VisibleState)
const VisibleState = {
  name: 'visible',
  toggle(modal) {
    console.log('弹窗隐藏');
    modal.setState(modal.hiddenState);
    document.body.classList.remove('modal-open');
  },
  showLoading(modal) {
    console.log('显示加载中');
    modal.setState(modal.loadingState);
  },
};

// 具体状态3:加载状态(LoadingState)
const LoadingState = {
  name: 'loading',
  toggle(modal) {
    console.log('加载中,无法切换');
    // 加载状态下不允许切换,无状态转换
  },
  showLoading(modal) {
    console.log('已在加载中');
  },
};

步骤 2:实现环境类(弹窗组件)

js 复制代码
// 环境类:弹窗(管理状态,委托行为)
class Modal {
  constructor() {
    // 初始化所有状态(状态实例可复用)
    this.hiddenState = HiddenState;
    this.visibleState = VisibleState;
    this.loadingState = LoadingState;
    // 默认状态:隐藏
    this.currentState = this.hiddenState;
  }

  // 切换状态(核心方法:修改当前状态)
  setState(state) {
    console.log(`状态切换:${this.currentState.name} → ${state.name}`);
    this.currentState = state;
  }

  // 外部接口:切换显示/隐藏(委托给当前状态)
  toggle() {
    this.currentState.toggle(this); // 传入环境类引用,便于状态转换
  }

  // 外部接口:显示加载(委托给当前状态)
  showLoading() {
    this.currentState.showLoading(this);
  }
}

步骤 3:使用状态模式

js 复制代码
const modal = new Modal();

// 初始状态:隐藏
modal.toggle(); // 弹窗显示 → 状态切换:hidden → visible
modal.showLoading(); // 显示加载中 → 状态切换:visible → loading
modal.toggle(); // 加载中,无法切换 → 无状态转换
modal.showLoading(); // 已在加载中 → 无状态转换
modal.toggle(); // 加载中,无法切换 → 无状态转换
modal.setState(modal.visibleState); // 手动切换状态:loading → visible
modal.toggle(); // 弹窗隐藏 → 状态切换:visible → hidden

// 状态切换:hidden → visible
// 弹窗显示
// 显示加载中
// 状态切换:visible → loading
// 加载中,无法切换
// 已在加载中
// 加载中,无法切换
// 状态切换:loading → visible
// 弹窗隐藏
// 状态切换:visible → hidden
  1. 进阶实现:类封装状态(复杂状态逻辑)

当状态需要维护内部数据(如加载进度、倒计时)时,用类封装状态(每个状态是独立实例),适合复杂场景(如游戏角色状态、多步骤表单)。

示例:游戏角色状态管理(奔跑 / 跳跃 / 攻击 / 受伤)

js 复制代码
// 状态接口:抽象类(定义统一行为)
class RoleState {
  constructor(role) {
    this.role = role; // 持有环境类引用(角色实例)
  }
  run() {} // 奔跑行为
  jump() {} // 跳跃行为
  attack() {} // 攻击行为
  hurt() {} // 受伤行为
}

// 具体状态1:待机状态(IdleState)
class IdleState extends RoleState {
  run() {
    console.log('从待机切换到奔跑');
    this.role.setState(new RunState(this.role));
  }
  jump() {
    console.log('从待机切换到跳跃');
    this.role.setState(new JumpState(this.role));
  }
  attack() {
    console.log('从待机切换到攻击');
    this.role.setState(new AttackState(this.role));
  }
  hurt() {
    console.log('待机时受伤');
    this.role.setState(new HurtState(this.role));
  }
}

// 具体状态2:奔跑状态(RunState)
class RunState extends RoleState {
  run() {
    console.log('已在奔跑');
  }
  jump() {
    console.log('从奔跑切换到跳跃');
    this.role.setState(new JumpState(this.role));
  }
  attack() {
    console.log('从奔跑切换到攻击');
    this.role.setState(new AttackState(this.role));
  }
  hurt() {
    console.log('奔跑时受伤,停止奔跑');
    this.role.setState(new HurtState(this.role));
  }
}

// 其他状态:JumpState、AttackState、HurtState 实现类似...
class JumpState extends RoleState {
  run() {
    console.log('跳跃中无法奔跑');
  }
  jump() {
    console.log('已在跳跃');
  }
  attack() {
    console.log('跳跃攻击');
  }
  hurt() {
    console.log('跳跃时受伤,掉落地面');
    this.role.setState(new HurtState(this.role));
  }
}

class AttackState extends RoleState {
  run() {
    console.log('攻击中无法奔跑');
  }
  jump() {
    console.log('攻击中无法跳跃');
  }
  attack() {
    console.log('连续攻击');
  }
  hurt() {
    console.log('攻击时受伤,中断攻击');
    this.role.setState(new HurtState(this.role));
  }
}

class HurtState extends RoleState {
  run() {
    console.log('受伤中无法奔跑');
  }
  jump() {
    console.log('受伤中无法跳跃');
  }
  attack() {
    console.log('受伤中无法攻击');
  }
  hurt() {
    console.log('连续受伤');
  }
}

// 环境类:游戏角色
class GameRole {
  constructor() {
    // 默认状态:待机
    this.currentState = new IdleState(this);
  }

  setState(state) {
    this.currentState = state;
  }

  // 外部接口(委托给当前状态)
  run() {
    this.currentState.run();
  }
  jump() {
    this.currentState.jump();
  }
  attack() {
    this.currentState.attack();
  }
  hurt() {
    this.currentState.hurt();
  }
}

// 使用:角色行为随状态自动切换
const role = new GameRole();
role.run(); // 从待机切换到奔跑
role.jump(); // 从奔跑切换到跳跃
role.attack(); // 跳跃攻击
role.hurt(); // 跳跃时受伤,掉落地面
role.run(); // 受伤中无法奔跑
  1. 前端框架适配:React/Vue 组件中的状态模式

在 React/Vue 中,组件的 state 本身就是 "状态容器",可结合状态模式封装复杂状态逻辑(如多步骤表单、组件生命周期状态),让组件代码更清晰。

示例:React 多步骤表单(状态模式封装步骤逻辑)。

js 复制代码
import { useState } from 'react';

// 具体状态1:步骤1(填写基本信息)
const Step1State = {
  name: 'step1',
  next(form) {
    // 验证表单
    if (!form.name || !form.phone) {
      alert('请填写完整信息');
      return false;
    }
    // 切换到步骤2
    form.setStep(Step2State);
    return true;
  },
  prev(form) {
    alert('已经是第一步');
    return false;
  },
  getFormFields() {
    return ['name', 'phone']; // 步骤1的表单字段
  },
};

// 具体状态2:步骤2(填写地址信息)
const Step2State = {
  name: 'step2',
  next(form) {
    if (!form.address) {
      alert('请填写地址');
      return false;
    }
    form.setStep(Step3State);
    return true;
  },
  prev(form) {
    form.setStep(Step1State);
    return true;
  },
  getFormFields() {
    return ['address', 'city']; // 步骤2的表单字段
  },
};

// 具体状态3:步骤3(确认提交)
const Step3State = {
  name: 'step3',
  next(form) {
    // 提交表单
    alert('表单提交成功!', form);
    return true;
  },
  prev(form) {
    form.setStep(Step2State);
    return true;
  },
  getFormFields() {
    return []; // 步骤3无表单字段,仅展示
  },
};

// React 组件(环境类)
function MultiStepForm() {
  // 表单数据
  const [formData, setFormData] = useState({
    name: '',
    phone: '',
    address: '',
    city: '',
  });
  // 当前状态(步骤)
  const [currentState, setCurrentState] = useState(Step1State);

  // 切换状态(步骤)
  const setStep = (state) => {
    setCurrentState(state);
  };

  // 委托行为给当前状态
  const handleNext = () => {
    currentState.next({ ...formData, setStep });
  };

  const handlePrev = () => {
    currentState.prev({ ...formData, setStep });
  };

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };

  // 渲染当前步骤的表单字段
  const renderFields = () => {
    const fields = currentState.getFormFields();
    return fields.map((field) => (
      <div key={field}>
        <label>{field}:</label>
        <input name={field} value={formData[field]} onChange={handleInputChange} />
      </div>
    ));
  };

  return (
    <div>
      <h3>步骤 {currentState.name.replace('step', '')}</h3>
      {renderFields()}
      <button onClick={handlePrev} disabled={currentState.name === 'step1'}>
        上一步
      </button>
      <button onClick={handleNext}>{currentState.name === 'step3' ? '提交' : '下一步'}</button>
    </div>
  );
}

前端典型应用场景:

  • 组件状态管理:弹窗(显示 / 隐藏 / 加载)、按钮(正常 / 禁用 / 加载)、表单(编辑 / 提交 / 成功 / 失败);
  • 多步骤流程:注册表单、支付流程、向导式操作(每步对应一个状态);
  • 游戏开发:角色状态(待机 / 奔跑 / 跳跃 / 攻击 / 受伤)、游戏场景状态(菜单 / 游戏中 / 暂停 / 结束);
  • 交互组件:轮播图(播放 / 暂停 / 切换中)、下拉菜单(展开 / 收起 / 禁用);
  • 状态机场景:任何需要 "状态驱动行为" 的场景(如工作流审批状态、订单状态流转)。

状态模式的核心优势:

  • 状态与行为分离:每种状态的行为封装在独立对象中,环境类无需关心状态判断,代码更简洁;
  • 易于扩展:新增状态时,只需添加新的状态对象 / 类,无需修改环境类和其他状态(符合 "开闭原则");
  • 状态转换清晰:状态转换逻辑封装在状态内部,便于维护和调试(如 "奔跑→跳跃" 的转换规则只在 RunState 中);
  • 避免条件判断:用状态委托替代冗长的 if-else/switch,提升代码可读性和可维护性;
  • 行为一致性:所有状态遵循统一接口,环境类通过相同的方法调用不同状态的行为,无需区分状态类型。

使用注意事项:

  • 状态数量控制:如果状态数量较少(如仅 2 种),用 if-else 可能更简洁,无需过度设计;
  • 状态接口统一:所有状态必须实现相同的行为接口(如都有 toggle()/next() 方法),否则无法灵活切换;
  • 避免状态过度复杂:如果单个状态逻辑过于复杂(如包含大量业务逻辑),可拆分状态或结合其他模式(如策略模式封装状态内的算法);
  • 防止状态循环:状态转换时需避免循环依赖(如 "状态 A→状态 B→状态 A"),必要时添加守卫条件;
  • 前端轻量实现:无需严格使用类结构,用对象 / 函数封装状态更符合前端开发习惯(如 React/Vue 示例)。

状态模式 vs 策略模式(核心区别)

模式 核心目标 关键区别
状态模式 状态驱动行为,状态可自动转换 策略是 "平等替代",状态是 "有顺序 / 依赖的转换"(如待机→奔跑→跳跃)
策略模式 封装可替换的算法,解耦选择逻辑 策略之间无依赖,使用者主动选择;状态之间可能有依赖,状态内部触发转换
关注点 对象的 "状态变化" 与 "行为联动" 算法的 "选择" 与 "实现分离"
示例场景 弹窗状态、角色状态、多步骤流程 表单验证、支付方式、数据格式化

状态模式的本质是 "状态封装 + 行为委托",核心价值在于让 "状态驱动行为" 的逻辑更清晰、易维护、可扩展。前端开发中,它特别适合处理多状态、状态转换复杂的组件或场景(如弹窗、多步骤表单、游戏角色),能有效替代臃肿的条件判断,让代码在状态变化中保持稳定。

实现状态模式时,前端开发者可灵活选择 "对象""类" 或 "组件状态" 的形式,无需拘泥于设计模式的标准类结构 ------ 核心是抓住 "状态决定行为,状态转换与环境类解耦" 的思想,让代码更符合 "单一职责原则" 和 "开闭原则"。

5. 命令模式

命令模式(Command Pattern)核心思想是:将 "请求" 封装为独立的 "命令对象",让请求的发送者(如用户操作)与接收者(如业务逻辑)解耦。命令对象包含执行请求的所有信息(接收者、方法、参数),支持请求的存储、传递、撤销 / 重做、批量执行等灵活操作。

类比生活场景:餐厅点餐时,顾客(请求发送者)不需要直接和厨师(请求接收者)沟通,只需将点菜单(命令对象)交给服务员(命令执行者),服务员再将菜单传递给厨师。点菜单包含了 "做什么菜""怎么做" 的所有信息,厨师只需按照菜单执行即可 ------ 这就是命令模式的核心:用 "命令对象" 作为中间载体,解耦发送者和接收者。

命令模式的核心结构

命令模式包含四个核心角色(前端实现会灵活简化,但思想不变):

角色 作用 前端实现形式
命令对象(Command) 封装请求的核心载体:包含接收者、执行方法(execute())、撤销方法(undo())。 类、对象或函数(包含执行逻辑)
请求发送者(Invoker) 触发命令的对象:持有命令对象,调用命令的 execute() 方法(无需知道接收者)。 按钮、菜单、函数(触发逻辑)
请求接收者(Receiver) 实际执行请求的对象:包含命令对应的业务逻辑(如删除数据、渲染页面)。 业务模块、工具类、组件实例
命令管理器(Optional) 管理命令队列:支持批量执行、撤销 / 重做、日志记录等高级功能。 数组、栈结构(存储命令历史)

核心交互流程:

  • 构建命令对象:将 "接收者 + 执行方法 + 参数" 封装到命令中;
  • 发送者触发命令:调用命令对象的 execute() 方法;
  • 命令对象调用接收者:命令内部调用接收者的对应方法,执行实际业务逻辑;
  • (可选)命令管理器记录命令:支持撤销、重做、批量执行等扩展操作。

没有命令模式时,请求的发送者与接收者直接耦合,会面临三大问题:

  • 耦合度高:发送者需要直接调用接收者的方法(如按钮点击事件直接调用 deleteData()),两者紧密关联,修改接收者会影响发送者;
  • 无法扩展:不支持请求的撤销 / 重做、批量执行、延迟执行(如定时任务);
  • 代码冗余:多个发送者需要触发相同请求时,需重复编写调用逻辑(如多个按钮触发同一删除操作)。
js 复制代码
// 反例(无命令模式的按钮操作):
// 接收者:数据管理模块(实际执行删除逻辑)
const DataManager = {
  deleteData: (id) => console.log(`删除数据 ${id}`),
  addData: (data) => console.log(`添加数据 ${JSON.stringify(data)}`),
};

// 发送者:按钮点击事件(直接调用接收者方法,耦合度高)
document.getElementById('delete-btn-1').addEventListener('click', () => {
  DataManager.deleteData(1); // 发送者直接依赖接收者
});

document.getElementById('delete-btn-2').addEventListener('click', () => {
  DataManager.deleteData(2); // 重复编写调用逻辑
});

前端典型应用场景

前端开发中,命令模式可根据需求灵活实现,从 "基础命令封装" 到 "高级命令管理",以下是四种典型场景:

  1. 基础实现:封装简单命令(解耦发送者与接收者)

适合简单场景(如按钮操作、单一请求),核心是用命令对象封装 "接收者 + 执行逻辑"。

步骤 1:定义接收者(实际业务逻辑)

js 复制代码
// 接收者1:数据管理模块(执行数据操作)
const DataReceiver = {
  delete: (id) => {
    console.log(`【接收者】删除数据 ID: ${id}`);
    // 实际业务逻辑:如调用接口、更新状态
    return { success: true, id };
  },
  add: (data) => {
    console.log(`【接收者】添加数据: ${JSON.stringify(data)}`);
    return { success: true, data };
  },
};

// 接收者2:日志模块(执行日志记录)
const LogReceiver = {
  record: (action, data) => {
    console.log(`【日志】记录操作: ${action} - ${JSON.stringify(data)}`);
  },
};

步骤 2:定义命令对象(封装请求)

js 复制代码
// 命令接口:抽象类(定义统一的 execute 方法)
class Command {
  constructor(receiver, method, ...params) {
    this.receiver = receiver; // 接收者(实际执行逻辑的对象)
    this.method = method; // 接收者的方法名
    this.params = params; // 方法参数
    this.result = null; // 执行结果(用于撤销/重做)
  }

  // 核心方法:执行命令
  execute() {
    console.log(`【命令】执行 ${this.method} 命令`);
    // 调用接收者的对应方法,保存执行结果
    this.result = this.receiver[this.method](...this.params);
    return this.result;
  }
}

// 具体命令1:删除数据命令(继承 Command 接口)
class DeleteCommand extends Command {
  constructor(id) {
    // 绑定接收者(DataReceiver)和方法(delete)
    super(DataReceiver, 'delete', id);
    this.id = id; // 额外存储参数,用于撤销
  }

  // 扩展:撤销命令(删除的逆操作是添加)
  undo() {
    console.log(`【命令】撤销删除命令(恢复数据 ID: ${this.id}`);
    DataReceiver.add({ id: this.id, name: `恢复的数据${this.id}` });
  }
}

// 具体命令2:添加数据命令
class AddCommand extends Command {
  constructor(data) {
    super(DataReceiver, 'add', data);
    this.data = data; // 存储数据,用于撤销
  }

  // 撤销命令(添加的逆操作是删除)
  undo() {
    console.log(`【命令】撤销添加命令(删除数据 ID: ${this.data.id}`);
    DataReceiver.delete(this.data.id);
  }
}

// 具体命令3:日志记录命令(无撤销需求)
class LogCommand extends Command {
  constructor(action, data) {
    super(LogReceiver, 'record', action, data);
  }
}

步骤 3:定义发送者(触发命令)

js 复制代码
// 发送者1:按钮(触发单个命令)
class Button {
  constructor(command) {
    this.command = command; // 持有命令对象
  }

  // 触发命令(如点击事件)
  click() {
    console.log(`【发送者】按钮点击,触发命令`);
    return this.command.execute();
  }
}

// 发送者2:菜单(触发多个命令,命令组合)
class MenuItem {
  constructor(commands) {
    this.commands = commands; // 持有多个命令对象
  }

  // 触发命令组合(如点击菜单执行"删除+日志")
  click() {
    console.log(`【发送者】菜单点击,触发命令组合`);
    return this.commands.map((cmd) => cmd.execute());
  }
}

步骤 4:使用命令模式

js 复制代码
// 场景1:单个按钮触发删除命令
const deleteCmd1 = new DeleteCommand(1);
const deleteBtn1 = new Button(deleteCmd1);
deleteBtn1.click();
// 输出:
// 【发送者】按钮点击,触发命令
// 【命令】执行 delete 命令
// 【接收者】删除数据 ID: 1

// 场景2:单个按钮触发添加命令
const addCmd = new AddCommand({ id: 3, name: '新数据' });
const addBtn = new Button(addCmd);
addBtn.click();
// 输出:
// 【发送者】按钮点击,触发命令
// 【命令】执行 add 命令
// 【接收者】添加数据: {"id":3,"name":"新数据"}

// 场景3:菜单触发命令组合(删除+日志)
const deleteCmd2 = new DeleteCommand(2);
const logCmd = new LogCommand('delete', { id: 2 });
const menuItem = new MenuItem([deleteCmd2, logCmd]);
menuItem.click();
// 输出:
// 【发送者】菜单点击,触发命令组合
// 【命令】执行 delete 命令
// 【接收者】删除数据 ID: 2
// 【命令】执行 record 命令
// 【日志】记录操作: delete - {"id":2}

// 场景4:撤销命令
deleteCmd1.undo();
// 输出:
// 【命令】撤销删除命令(恢复数据 ID: 1)
// 【接收者】添加数据: {"id":1,"name":"恢复的数据1"}
  1. 进阶实现:命令管理器(支持撤销 / 重做 / 批量执行)

对于需要 "撤销 / 重做""批量执行""命令日志" 的场景(如编辑器、表单操作),可添加 "命令管理器" 管理命令队列。

js 复制代码
class CommandManager {
  constructor() {
    this.commandQueue = []; // 已执行的命令队列(用于撤销)
    this.redoQueue = []; // 已撤销的命令队列(用于重做)
  }

  // 执行命令并记录到队列
  executeCommand(command) {
    const result = command.execute();
    this.commandQueue.push(command);
    this.redoQueue = []; // 执行新命令后,清空重做队列(避免重做旧命令)
    return result;
  }

  // 撤销最后一个命令
  undo() {
    if (this.commandQueue.length === 0) {
      console.log('【管理器】无命令可撤销');
      return;
    }
    const command = this.commandQueue.pop();
    if (command.undo) {
      // 仅支持有 undo 方法的命令
      command.undo();
      this.redoQueue.push(command); // 存入重做队列
    } else {
      console.log('【管理器】该命令不支持撤销');
    }
  }

  // 重做最后一个撤销的命令
  redo() {
    if (this.redoQueue.length === 0) {
      console.log('【管理器】无命令可重做');
      return;
    }
    const command = this.redoQueue.pop();
    command.execute();
    this.commandQueue.push(command); // 重新存入执行队列
  }

  // 批量执行命令
  executeBatch(commands) {
    console.log('【管理器】批量执行命令');
    commands.forEach((cmd) => this.executeCommand(cmd));
  }

  // 清空所有命令
  clear() {
    this.commandQueue = [];
    this.redoQueue = [];
    console.log('【管理器】清空所有命令队列');
  }
}

使用命令管理器(编辑器撤销 / 重做场景)

js 复制代码
// 初始化命令管理器
const cmdManager = new CommandManager();

// 执行一系列命令
cmdManager.executeCommand(new AddCommand({ id: 4, name: '文档1' }));
cmdManager.executeCommand(new AddCommand({ id: 5, name: '文档2' }));
cmdManager.executeCommand(new DeleteCommand(4));

// 撤销操作
cmdManager.undo(); // 撤销删除命令(恢复文档4)
cmdManager.undo(); // 撤销添加文档2的命令

// 重做操作
cmdManager.redo(); // 重做添加文档2的命令

// 批量执行命令
const batchCommands = [
  new AddCommand({ id: 6, name: '批量文档1' }),
  new AddCommand({ id: 7, name: '批量文档2' }),
  new LogCommand('batchAdd', [6, 7]),
];
cmdManager.executeBatch(batchCommands);

// 输出结果:
// 【命令】执行 add 命令 → 添加文档4
// 【命令】执行 add 命令 → 添加文档5
// 【命令】执行 delete 命令 → 删除文档4
// 【命令】撤销删除命令 → 恢复文档4
// 【命令】撤销添加命令 → 删除文档5
// 【命令】执行 add 命令 → 重新添加文档5
// 【管理器】批量执行命令 → 添加批量文档1、2,记录日志
  1. 简化实现:无接收者的命令(函数封装)

对于简单场景(无需复杂接收者、仅需封装逻辑),可省略独立接收者,直接用命令对象封装执行函数,代码更简洁。

js 复制代码
// 简化命令:直接封装执行逻辑(无独立接收者)
class SimpleCommand {
  constructor(executeFn, undoFn) {
    this.executeFn = executeFn; // 执行函数
    this.undoFn = undoFn; // 撤销函数(可选)
  }

  execute() {
    return this.executeFn();
  }

  undo() {
    if (this.undoFn) return this.undoFn();
    console.log('不支持撤销');
  }
}

// 使用简化命令(切换主题场景)
let currentTheme = 'light';

// 切换深色模式命令
const darkThemeCmd = new SimpleCommand(
  () => {
    // 执行逻辑
    currentTheme = 'dark';
    console.log('切换到深色模式');
  },
  () => {
    // 撤销逻辑
    currentTheme = 'light';
    console.log('撤销,切换到浅色模式');
  },
);

// 发送者:按钮
const themeBtn = new Button(darkThemeCmd);
themeBtn.click(); // 切换到深色模式
darkThemeCmd.undo(); // 撤销,切换到浅色模式
  1. 前端框架适配:React/Vue 中的命令模式

在 React/Vue 中,命令模式可用于封装 "复杂用户操作"(如表单提交、列表操作),支持撤销 / 重做、状态回滚,让组件逻辑更清晰。

js 复制代码
import { useState } from 'react';

// 接收者:列表数据管理
const ListReceiver = {
  data: [1, 2, 3],
  delete: (index) => {
    const deletedItem = ListReceiver.data.splice(index, 1)[0];
    return deletedItem; // 返回删除的元素,用于撤销
  },
  restore: (index, item) => {
    ListReceiver.data.splice(index, 0, item); // 恢复元素
  },
};

// 命令:删除列表项命令
class DeleteItemCommand {
  constructor(index) {
    this.index = index;
    this.deletedItem = null; // 存储删除的元素
  }

  execute() {
    this.deletedItem = ListReceiver.delete(this.index);
    console.log(`删除索引 ${this.index} 的元素: ${this.deletedItem}`);
    return ListReceiver.data;
  }

  undo() {
    if (this.deletedItem === null) return;
    ListReceiver.restore(this.index, this.deletedItem);
    console.log(`恢复索引 ${this.index} 的元素: ${this.deletedItem}`);
    return ListReceiver.data;
  }
}

// 命令管理器(简化版)
const useCommandManager = () => {
  const [commandQueue, setCommandQueue] = useState([]);
  const [redoQueue, setRedoQueue] = useState([]);

  const execute = (command) => {
    const newData = command.execute();
    setCommandQueue((prev) => [...prev, command]);
    setRedoQueue([]);
    return newData;
  };

  const undo = () => {
    if (commandQueue.length === 0) return;
    const lastCmd = commandQueue[commandQueue.length - 1];
    const newData = lastCmd.undo();
    setCommandQueue((prev) => prev.slice(0, -1));
    setRedoQueue((prev) => [...prev, lastCmd]);
    return newData;
  };

  return { execute, undo };
};

// React 组件(发送者)
function ListComponent() {
  const [list, setList] = useState(ListReceiver.data);
  const { execute, undo } = useCommandManager();

  const handleDelete = (index) => {
    const cmd = new DeleteItemCommand(index);
    const newData = execute(cmd);
    setList([...newData]);
  };

  const handleUndo = () => {
    const newData = undo();
    setList([...newData]);
  };

  return (
    <div>
      <ul>
        {list.map((item, index) => (
          <li key={index}>
            {item}
            <button onClick={() => handleDelete(index)}>删除</button>
          </li>
        ))}
      </ul>
      <button onClick={handleUndo} disabled={!undo}>
        撤销删除
      </button>
    </div>
  );
}

前端典型应用场景

  • 用户操作撤销 / 重做:编辑器(文本编辑、图形编辑)、表单操作(输入内容撤销)、列表操作(删除 / 添加撤销);
  • 批量操作:批量删除、批量保存、批量导出(将多个单个命令组合为批量命令);
  • 延迟执行 / 定时任务:定时器命令(如 5 秒后执行保存)、异步请求命令(如接口请求完成后执行后续命令);
  • 组件交互解耦:父子组件、跨组件通信(如子组件触发命令,父组件作为接收者执行逻辑,无需直接传参);
  • 日志记录与回放:记录用户操作命令,用于故障排查、操作回放(如后台系统记录管理员操作日志);
  • 状态机 / 工作流:复杂流程的步骤封装(如订单提交流程:验证→支付→通知,每个步骤封装为命令)。

命令模式的核心优势:

  • 解耦发送者与接收者:发送者无需知道接收者的具体实现(如按钮无需知道数据如何删除),只需触发命令,降低代码耦合;
  • 支持灵活扩展:命令可组合(如 "删除 + 日志" 组合命令);
  • 支持撤销 / 重做(通过命令队列记录历史操作);
  • 支持延迟执行(如定时命令)、批量执行(如批量删除);
  • 便于管理请求:命令对象可存储、传递、序列化(如日志记录命令历史,用于故障恢复);
  • 代码复用:相同的请求可封装为命令对象,在多个发送者中复用(如多个按钮触发同一删除命令);
  • 符合开闭原则:新增命令时,无需修改发送者、接收者或管理器,只需添加新的命令类。

命令模式 vs 其他模式(核心区别)

模式 核心目标 关键区别
命令模式 封装请求为对象,解耦发送者与接收者,支持撤销 / 批量执行 关注 "请求的封装与管理",命令是独立对象,可存储 / 传递
策略模式 封装可替换的算法,解耦选择逻辑 关注 "算法的替换",策略是平等的,无顺序依赖
观察者模式 状态变化通知依赖者,实现一对多通信 关注 "状态联动",无需封装请求,被动通知
代理模式 控制对原对象的访问,添加额外逻辑 关注 "访问控制",代理对象与原对象接口一致

使用注意事项:

  • 避免过度设计:简单场景(如单个按钮触发单个函数)无需使用命令模式,直接调用函数更高效;
  • 命令接口统一:所有命令需遵循相同的接口(如 execute()/undo()),确保管理器可统一处理;
  • 控制命令复杂度:单个命令不应包含过多逻辑,复杂命令可拆分为多个简单命令组合(如 "提交表单" 拆分为 "验证 + 保存 + 通知");
  • 内存考量:命令队列会存储历史命令,若命令数量庞大(如长时间编辑),需定期清理或序列化存储(如存入本地存储);
  • 撤销 / 重做的实现成本:并非所有命令都支持撤销(如接口请求发送后无法撤回),需根据业务场景设计可逆操作(如删除数据时先备份,撤销时恢复备份)。

命令模式的本质是 "请求的封装与管理",核心价值在于解耦发送者与接收者,并支持撤销、批量执行、延迟执行等灵活操作。前端开发中,它特别适合处理 "需要记录操作历史""复杂用户交互""跨组件解耦" 的场景(如编辑器、表单、列表操作)。

实现命令模式时,前端开发者可根据需求灵活选择 "完整类结构""简化函数封装" 或 "框架适配版"------ 核心是抓住 "命令对象封装请求信息" 的思想,让代码更易扩展、维护,同时支持业务所需的高级功能(如撤销 / 重做)。

命令模式的核心不是 "代码有多简洁",而是 "逻辑有多灵活"------ 它让原本直接耦合的 "触发操作" 和 "执行逻辑",变成了可管理、可扩展的独立单元,这也是它在复杂交互场景中不可或缺的原因。

相关推荐
ZHE|张恒2 小时前
设计模式(五)原型模式 — 通过克隆快速复制对象,避免复杂初始化
设计模式·原型模式
敖云岚16 小时前
【设计模式】简单易懂的行为型设计模式-策略模式
设计模式·策略模式
IT永勇1 天前
C++设计模式-单例
c++·单例模式·设计模式
ZHE|张恒1 天前
设计模式(四)建造者模式 — 分步骤构建复杂对象,让创建过程可控可扩展
设计模式·建造者模式
ZHE|张恒1 天前
设计模式(三)抽象工厂模式 — 一次性创建一整套相关对象的终极工厂
设计模式·抽象工厂模式
崎岖Qiu1 天前
状态模式与策略模式的快速区分与应用
笔记·设计模式·状态模式·策略模式·开闭原则
明洞日记2 天前
【设计模式手册007】原型模式 - 通过复制创建对象的艺术
java·设计模式·原型模式
u***j3242 天前
算法设计模式总结
算法·设计模式
烤麻辣烫2 天前
23种设计模式(新手)-7迪米特原则 合成复用原则
java·开发语言·学习·设计模式·intellij-idea