JavaScript 设计模式与应用

中等 🟡Js/Ts
12 个标签
预计阅读时间:229 分钟
JavaScript设计模式单例工厂观察者策略装饰器代理结构型行为型创建型函数式模式

JavaScript 设计模式与应用

设计模式是解决软件设计中常见问题的可重用方案。在 JavaScript 中,设计模式不仅可以帮助我们编写更加优雅、可维护的代码,还能提高代码的可读性和可扩展性。JavaScript 的动态特性使得某些设计模式的实现与传统面向对象语言有所不同,理解这些差异对于正确应用设计模式至关重要。

为什么要学习设计模式

设计模式最早由 GoF(Gang of Four,四人组)在《设计模式:可复用面向对象软件的基础》一书中系统总结,共 23 种经典模式。虽然这本书以 C++ 和 Smalltalk 为例,但其思想是跨语言的。对于 JavaScript 开发者而言,设计模式的价值主要体现在以下几个方面:

提供共同词汇:当你说"这里用了观察者模式"时,团队成员立刻就能理解代码的结构和意图,而不需要逐行阅读实现。设计模式是工程师之间沟通的"行话"。
沉淀最佳实践:每一个设计模式都是无数工程师在实践中反复踩坑后总结出来的经验结晶,遵循它们可以避免重复造轮子和重复犯错。
提升可维护性与可扩展性:好的设计模式往往遵循"对扩展开放、对修改关闭"的开闭原则,让代码在需求变化时更容易演进。
理解框架源码:React 的 Hooks 与虚拟 DOM、Vue 的响应式系统、Redux 的单向数据流、Express 的中间件机制,背后都是设计模式的组合运用。理解设计模式能让你更快看懂框架源码。

需要强调的是:设计模式不是银弹,也不是越多越好。滥用设计模式(over-engineering,过度设计)比不用设计模式危害更大。本文会在每个模式的"常见坑"小节反复强调这一点。

设计模式的分类

GoF 将 23 种设计模式按照"目的"分为三大类,这个分类体系至今仍是最主流的划分方式:

| 分类 | 英文 | 核心关注点 | 包含模式 |

| --- | --- | --- | --- |

| 创建型模式 | Creational | 对象如何被创建,解耦创建与使用 | 单例、工厂方法、抽象工厂、建造者、原型 |

| 结构型模式 | Structural | 类与对象如何组合成更大结构 | 适配器、桥接、组合、装饰器、外观、享元、代理 |

| 行为型模式 | Behavioral | 对象之间如何交互、分配职责 | 责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者 |

除了 GoF 的 23 种,JavaScript 社区还沉淀了一批"语言特色"模式,例如模块模式、揭示模块模式、混入(Mixin)、发布订阅、依赖注入以及来自函数式编程的柯里化、函数组合等。这些模式充分利用了 JavaScript 的闭包、原型链和一等函数特性,是日常前端开发中出现频率最高的模式,本文后半部分会专门讲解。

设计原则:SOLID 与模式的关系

设计模式是"术",而支撑这些模式的"道"是一系列设计原则。最著名的是 SOLID 五大原则:

| 原则 | 全称 | 一句话解释 | 相关模式 |

| --- | --- | --- | --- |

| S | 单一职责原则 | 一个类只应有一个引起它变化的原因 | 外观、命令 |

| O | 开闭原则 | 对扩展开放,对修改关闭 | 策略、装饰器、工厂 |

| L | 里氏替换原则 | 子类必须能替换父类而不破坏程序 | 模板方法、工厂方法 |

| I | 接口隔离原则 | 不应强迫依赖它不需要的接口 | 适配器、桥接 |

| D | 依赖倒置原则 | 依赖抽象而非具体实现 | 依赖注入、工厂、策略 |

此外还有一些常被提及的补充原则:DRY(Don't Repeat Yourself,不要重复自己)、KISS(Keep It Simple, Stupid,保持简单)、YAGNI(You Aren't Gonna Need It,你不会需要它)以及迪米特法则(最少知识原则,一个对象应尽量少地了解其他对象)。这些原则共同指导我们判断"何时该用模式、何时不该用"。

javascriptCode
// 违反开闭原则的写法:每新增一种支付方式都要改这个函数
function pay(type, amount) {
  if (type === 'wechat') {
    // 微信支付逻辑
  } else if (type === 'alipay') {
    // 支付宝逻辑
  } else if (type === 'card') {
    // 银行卡逻辑
  }
  // 新增 PayPal 就要继续加 else if... 违反开闭原则
}

// 遵循开闭原则的写法:新增支付方式只需注册新策略,不改原有代码
const payStrategies = new Map();
function registerPay(type, handler) {
  payStrategies.set(type, handler);
}
function pay(type, amount) {
  const handler = payStrategies.get(type);
  if (!handler) throw new Error(`Unsupported pay type: ${type}`);
  return handler(amount);
}
registerPay('wechat', (amount) => console.log(`微信支付 ${amount}`));
registerPay('alipay', (amount) => console.log(`支付宝支付 ${amount}`));
// 新增 PayPal,原有代码一行都不用动
registerPay('paypal', (amount) => console.log(`PayPal 支付 ${amount}`));

如何阅读本文

本文对每个模式都遵循统一的讲解结构,方便你系统学习和查阅:概念与类比 → 为什么重要 → 原理与结构 → 代码示例(多个变体)→ 真实案例 → 与相近模式对比 → 常见坑 → 最佳实践。你可以顺序通读建立体系,也可以把它当作速查手册,遇到具体问题时直接跳到对应模式。下面从创建型模式开始。

创建型模式

创建型模式关注对象的创建过程,其核心目标是将对象的"创建"和"使用"解耦。当一个系统不再关心对象是如何被创建、组合和表示时,它就获得了更大的灵活性。创建型模式回答的是"由谁创建、创建什么、何时创建、如何创建"这几个问题。JavaScript 由于其动态特性和一等函数,很多创建型模式可以用比传统语言更轻量的方式实现。

单例模式(Singleton):

单例模式确保一个类只有一个实例,并提供全局访问点。在 JavaScript 中,单例模式常用于管理全局状态、配置对象、数据库连接池等场景。实现单例模式的关键是控制实例的创建过程,确保多次调用构造函数时返回同一个实例。

javascriptCode
// 单例模式实现
class Singleton {
  static instance = null;
  
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
    this.data = {};
  }
  
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

// 使用示例
const instance1 = new Singleton();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

// 模块模式实现单例
const ConfigManager = (function() {
  let instance;
  
  function createInstance() {
    const config = {};
    return {
      get: (key) => config[key],
      set: (key, value) => { config[key] = value; },
      getAll: () => ({ ...config }),
    };
  }
  
  return {
    getInstance: () => {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

// ES6 模块天然是单例
// config.js
export const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
};

单例模式:概念类比与深入理解

理解单例最好的类比是"公司的 CEO"或"国家的政府":无论谁去访问,得到的都是同一个 CEO、同一个政府,不会因为不同的人访问而产生多个 CEO。单例模式保证的正是这种"全局唯一性"。在软件里,很多资源天然就应该是唯一的:全局配置、日志记录器、数据库连接池、浏览器里的 window 和 document、Redux 的 store、Vuex 的 store。

为什么单例模式重要:

节约资源:某些对象创建成本很高(如数据库连接、线程池),重复创建会浪费内存和 CPU。
保证状态一致:全局状态如果存在多份副本,很容易出现数据不一致的诡异 bug。单例保证所有代码读写的是同一份状态。
提供统一访问点:单例通常暴露一个静态方法或全局变量作为访问入口,任何模块都能方便地拿到它。

单例模式的结构(文字版 UML):

单例类内部持有一个指向自身唯一实例的静态引用(instance),构造函数被"私有化"或加以控制,外部只能通过静态方法 getInstance() 获取实例。第一次调用时创建并缓存实例(懒加载),之后每次调用都返回缓存的实例。结构可以概括为:Singleton 类 —持有→ 静态 instance 字段;Singleton 类 —暴露→ getInstance() 静态方法。

更多代码变体:

javascriptCode
// 变体一:闭包 + IIFE 实现懒加载单例(经典写法)
const Logger = (function () {
  let instance = null;

  function createLogger() {
    const logs = [];
    return {
      log(message) {
        const entry = `[${new Date().toISOString()}] ${message}`;
        logs.push(entry);
        console.log(entry);
      },
      getLogCount() {
        return logs.length;
      },
    };
  }

  return {
    getInstance() {
      if (!instance) {
        instance = createLogger();
      }
      return instance;
    },
  };
})();

Logger.getInstance().log('应用启动');
Logger.getInstance().log('用户登录');
console.log(Logger.getInstance().getLogCount()); // 2,两次拿到的是同一实例
javascriptCode
// 变体二:使用 Symbol 与 Object.freeze 保护单例,防止被篡改
class AppConfig {
  constructor() {
    if (AppConfig._instance) {
      return AppConfig._instance;
    }
    this.settings = {
      theme: 'dark',
      language: 'zh-CN',
      apiBase: 'https://api.example.com',
    };
    AppConfig._instance = this;
    Object.freeze(this); // 冻结,禁止外部修改属性
  }

  get(key) {
    return this.settings[key];
  }
}

// 变体三:现代前端最常用——ES Module 天然单例
// store.js 中导出的对象在整个应用生命周期内只有一份
// export const store = createStore();
// 任何模块 import { store } 拿到的都是同一个引用,无需任何额外代码
javascriptCode
// 变体四:惰性单例的通用封装(提取"单例"这一横切逻辑)
function createSingleton(factory) {
  let instance = null;
  return function (...args) {
    if (!instance) {
      instance = factory(...args);
    }
    return instance;
  };
}

const getDatabase = createSingleton((connectionString) => {
  console.log('建立数据库连接:', connectionString);
  return { query: (sql) => console.log('执行:', sql) };
});

const db1 = getDatabase('mongodb://localhost');
const db2 = getDatabase('mongodb://localhost'); // 不会再次建立连接
console.log(db1 === db2); // true

真实案例:

Redux 的 store:整个应用只有一个 store,所有组件从同一个 store 读取状态、派发 action,这就是单例模式在状态管理中的应用。
浏览器的 window/document/navigator:这些宿主对象都是全局唯一的单例。
数据库连接池:Node.js 后端里,mongoose、pg 等库通常维护一个全局连接池单例,避免为每次请求都新建连接。
前端的全局弹窗/消息提示:像 Element UI 的 Message、Ant Design 的 message,通常内部维护一个单例容器,避免创建多个重叠的提示层。

单例与相近模式对比:

| 对比项 | 单例模式 | 静态工具类 | 全局变量 |

| --- | --- | --- | --- |

| 是否可延迟初始化 | 是(懒加载) | 否 | 否 |

| 是否可实现接口/多态 | 是 | 否 | 否 |

| 是否可被继承扩展 | 较难 | 否 | 否 |

| 命名空间污染风险 | 低 | 低 | 高 |

| 单元测试友好度 | 中(需重置实例) | 中 | 低 |

常见坑:

多模块打包导致"伪多例":如果构建配置有误,同一个单例模块被打包进多个 chunk,可能出现多个"单例"实例。要确保依赖被正确去重。
单元测试污染:单例持有全局状态,一个测试修改了单例状态会影响后续测试。测试时应提供重置实例的机制(如 `Singleton._instance = null`)。
隐藏依赖:到处直接引用全局单例会造成隐式耦合,难以追踪谁在修改状态,也不利于依赖注入和 mock。
并发场景(Node.js worker/多线程)下单例并不跨进程共享:每个进程有自己的单例,需要用外部存储(如 Redis)来实现真正的跨进程唯一。

最佳实践:

优先使用 ES Module 的天然单例特性,而不是手写复杂的单例类。
对确实需要全局唯一的资源(配置、连接池、日志器)才用单例,不要把单例当成"全局变量"的借口。
为单例提供清晰的初始化和销毁/重置接口,方便测试。
结合依赖注入,把单例作为依赖显式传入,而不是在模块内部到处直接 import,以降低耦合。

工厂模式(Factory):

工厂模式封装对象的创建过程,提供统一的创建接口,隐藏具体实现细节。工厂模式分为简单工厂、工厂方法和抽象工厂三种。在 JavaScript 中,工厂模式常用于创建复杂对象、根据条件创建不同类型的对象等场景。

javascriptCode
// 简单工厂
class ButtonFactory {
  static create(type) {
    switch (type) {
      case 'primary':
        return new PrimaryButton();
      case 'secondary':
        return new SecondaryButton();
      case 'danger':
        return new DangerButton();
      default:
        throw new Error(`Unknown button type: ${type}`);
    }
  }
}

// 工厂方法
class Dialog {
  createButton() {
    throw new Error('Subclass must implement createButton method');
  }
  
  render() {
    const button = this.createButton();
    button.render();
  }
}

class WindowsDialog extends Dialog {
  createButton() {
    return new WindowsButton();
  }
}

class WebDialog extends Dialog {
  createButton() {
    return new HTMLButton();
  }
}

// 抽象工厂
class GUIFactory {
  createButton() {}
  createCheckbox() {}
}

class WindowsFactory extends GUIFactory {
  createButton() {
    return new WindowsButton();
  }
  createCheckbox() {
    return new WindowsCheckbox();
  }
}

class MacOSFactory extends GUIFactory {
  createButton() {
    return new MacOSButton();
  }
  createCheckbox() {
    return new MacOSCheckbox();
  }
}

工厂模式:概念类比与深入理解

工厂模式的类比就是现实世界的"工厂":你去餐厅点一杯"拿铁",你不需要知道咖啡机内部如何研磨、萃取、打奶泡,你只要告诉服务员"我要拿铁",厨房(工厂)就把成品交给你。你与"具体的制作过程"完全解耦。在代码里,工厂就是那个"你告诉它要什么类型,它负责 new 出对应对象"的角色。

工厂模式其实是三个相关但不同的模式的统称:

| 名称 | 意图 | 复杂度 | 典型用法 |

| --- | --- | --- | --- |

| 简单工厂(Simple Factory) | 用一个函数/方法根据参数返回不同类的实例 | 低 | 一个 create(type) 方法内部 switch |

| 工厂方法(Factory Method) | 定义创建对象的接口,让子类决定实例化哪个类 | 中 | 抽象基类声明 createXxx(),子类各自实现 |

| 抽象工厂(Abstract Factory) | 提供创建一系列相关对象的接口,不指定具体类 | 高 | 一个工厂创建一整套配套产品(按钮+复选框+对话框) |

为什么工厂模式重要:

解耦创建与使用:调用方只依赖工厂接口,不依赖具体产品类,符合依赖倒置原则。
集中管理创建逻辑:所有 new 的细节集中在工厂里,未来更换实现、增加日志、加缓存都只改一处。
便于扩展:新增一种产品,只需新增一个产品类并在工厂里注册,符合开闭原则(尤其是配合注册表实现时)。

工厂模式的结构(文字版 UML):

以工厂方法为例:抽象产品接口 Product 定义产品的行为;具体产品 ConcreteProductA、ConcreteProductB 实现该接口;抽象创建者 Creator 声明工厂方法 factoryMethod()(返回 Product 类型);具体创建者 ConcreteCreatorA、ConcreteCreatorB 覆写 factoryMethod() 分别返回不同的具体产品。关系为:Creator —依赖→ Product;ConcreteCreator —创建→ ConcreteProduct。

更多代码变体:

javascriptCode
// 变体一:用注册表替代 switch,实现开闭原则
class ShapeFactory {
  constructor() {
    this.creators = new Map();
  }

  register(type, creator) {
    this.creators.set(type, creator);
    return this;
  }

  create(type, ...args) {
    const creator = this.creators.get(type);
    if (!creator) {
      throw new Error(`Unknown shape type: ${type}`);
    }
    return creator(...args);
  }
}

const factory = new ShapeFactory()
  .register('circle', (r) => ({ type: 'circle', area: () => Math.PI * r * r }))
  .register('square', (s) => ({ type: 'square', area: () => s * s }))
  .register('rect', (w, h) => ({ type: 'rect', area: () => w * h }));

const circle = factory.create('circle', 5);
console.log(circle.area()); // 78.53...
// 新增三角形:factory.register('triangle', ...),无需修改工厂内部
javascriptCode
// 变体二:函数式简单工厂——返回不同的对象字面量
function createUser(role) {
  const base = {
    login() {
      console.log(`${this.name} 登录`);
    },
  };

  const roleConfigs = {
    admin: { name: '管理员', permissions: ['read', 'write', 'delete'] },
    editor: { name: '编辑', permissions: ['read', 'write'] },
    viewer: { name: '访客', permissions: ['read'] },
  };

  const config = roleConfigs[role];
  if (!config) throw new Error(`Unknown role: ${role}`);

  return { ...base, ...config, role };
}

const admin = createUser('admin');
console.log(admin.permissions); // ['read', 'write', 'delete']
javascriptCode
// 变体三:抽象工厂——创建一整套主题化的 UI 组件
class DarkThemeFactory {
  createButton() {
    return { render: () => console.log('渲染深色按钮') };
  }
  createInput() {
    return { render: () => console.log('渲染深色输入框') };
  }
}

class LightThemeFactory {
  createButton() {
    return { render: () => console.log('渲染浅色按钮') };
  }
  createInput() {
    return { render: () => console.log('渲染浅色输入框') };
  }
}

function renderForm(factory) {
  factory.createButton().render();
  factory.createInput().render();
}

const theme = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches
  ? new DarkThemeFactory()
  : new LightThemeFactory();
renderForm(theme); // 一整套组件风格保持一致

真实案例:

React.createElement / JSX:JSX 编译后调用 React.createElement,本质就是一个元素工厂,根据传入的类型(字符串或组件)创建对应的虚拟 DOM 节点。
document.createElement(tag):浏览器根据传入的标签名返回不同类型的 DOM 元素对象,这是最典型的简单工厂。
axios.create(config):根据配置创建一个定制化的请求实例,是工厂方法的应用。
数据库驱动:ORM(如 Sequelize)根据配置里的 dialect(mysql/postgres/sqlite)创建对应的方言适配器,是抽象工厂的应用。

工厂与相近模式对比:

| 对比项 | 简单工厂 | 工厂方法 | 抽象工厂 | 建造者模式 |

| --- | --- | --- | --- | --- |

| 关注点 | 创建单个产品 | 创建单个产品,延迟到子类 | 创建产品族 | 分步骤构建复杂对象 |

| 是否符合开闭原则 | 否(改 switch) | 是 | 是 | 是 |

| 客户端感知具体类 | 感知 | 不感知 | 不感知 | 不感知 |

| 适用场景 | 类型少且稳定 | 类型可能扩展 | 多套配套产品 | 参数多、构建步骤复杂 |

建造者模式(Builder)补充:

建造者模式常与工厂并列讨论,它用于"分步骤构建一个复杂对象"。当一个对象有很多可选参数时,用建造者比用巨长的构造函数参数列表清晰得多。

javascriptCode
// 建造者模式:链式构建复杂的查询对象
class QueryBuilder {
  constructor(table) {
    this.query = { table, wheres: [], orders: [], limitVal: null };
  }

  where(field, op, value) {
    this.query.wheres.push({ field, op, value });
    return this;
  }

  orderBy(field, dir = 'ASC') {
    this.query.orders.push({ field, dir });
    return this;
  }

  limit(n) {
    this.query.limitVal = n;
    return this;
  }

  build() {
    let sql = `SELECT * FROM ${this.query.table}`;
    if (this.query.wheres.length) {
      const conditions = this.query.wheres
        .map((w) => `${w.field} ${w.op} '${w.value}'`)
        .join(' AND ');
      sql += ` WHERE ${conditions}`;
    }
    if (this.query.orders.length) {
      const orders = this.query.orders
        .map((o) => `${o.field} ${o.dir}`)
        .join(', ');
      sql += ` ORDER BY ${orders}`;
    }
    if (this.query.limitVal != null) {
      sql += ` LIMIT ${this.query.limitVal}`;
    }
    return sql;
  }
}

const sql = new QueryBuilder('users')
  .where('age', '>', 18)
  .where('status', '=', 'active')
  .orderBy('created_at', 'DESC')
  .limit(10)
  .build();
console.log(sql);

常见坑:

简单工厂里的 switch 越来越长:这是最常见的问题,每加一个类型就改一次 switch,违反开闭原则。解决办法是改用注册表(Map)方式。
工厂返回的对象类型不一致:如果工厂在不同分支返回结构差异很大的对象,调用方难以统一处理。应保证所有产品实现相同的接口。
过度使用抽象工厂:只有一套产品时用抽象工厂纯属过度设计,简单工厂或直接 new 就够了。

最佳实践:

类型稳定且数量少时用简单工厂即可,不必上工厂方法。
需要"新增类型不改旧代码"时,用注册表式工厂或工厂方法。
需要创建"多套配套的产品族"时才考虑抽象工厂。
构造参数多、构建过程复杂时,用建造者模式替代长参数构造函数。

构造器模式(Constructor):

构造器模式使用构造函数创建对象,初始化对象状态。在 JavaScript 中,构造函数可以与原型链配合实现属性和方法的共享,减少内存消耗。ES6 的 class 语法是构造函数的语法糖,使代码更加清晰。

javascriptCode
// ES5 构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, I'm ${this.name}`);
};

Person.species = 'Homo sapiens'; // 静态属性

// ES6 类语法
class Person {
  static species = 'Homo sapiens';
  
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
  
  static getSpecies() {
    return Person.species;
  }
}

const person = new Person('Alice', 25);
person.greet(); // "Hello, I'm Alice"

构造器模式:概念类比与深入理解

构造器模式可以类比"盖房子用的图纸":构造函数就是图纸,规定了每一栋房子(实例)应该有哪些属性(几间卧室、几个卫生间)和初始状态。用同一张图纸(构造函数)配合 new,就能盖出一栋栋结构相同但内容各异的房子(实例)。

为什么构造器模式重要:

标准化对象初始化:把"创建对象 + 初始化状态"的逻辑集中到构造函数里,保证每个实例都被正确初始化。
配合原型链共享方法:把方法定义在原型上,所有实例共享同一份方法,避免每个实例都复制一份方法造成的内存浪费。
支持 instanceof 类型判断:通过构造函数创建的对象可以用 instanceof 检查类型,便于运行时类型判断。

new 操作符到底做了什么(原理):

理解构造器模式必须理解 new 的四个步骤:

1.创建一个全新的空对象;
2.把这个新对象的 [[Prototype]](`__proto__`)指向构造函数的 prototype 属性;
3.以这个新对象作为 this 执行构造函数体;
4.如果构造函数返回的是对象则返回该对象,否则返回步骤 1 创建的新对象。
javascriptCode
// 手写一个 myNew,彻底理解 new 的原理
function myNew(Constructor, ...args) {
  // 步骤 1、2:创建对象并链接原型
  const obj = Object.create(Constructor.prototype);
  // 步骤 3:以新对象为 this 执行构造函数
  const result = Constructor.apply(obj, args);
  // 步骤 4:如果构造函数返回对象则用它,否则返回 obj
  return result !== null && typeof result === 'object' ? result : obj;
}

function Car(brand, price) {
  this.brand = brand;
  this.price = price;
}
Car.prototype.info = function () {
  return `${this.brand}: ${this.price}`;
};

const car = myNew(Car, 'Tesla', 300000);
console.log(car.info()); // Tesla: 300000
console.log(car instanceof Car); // true

更多代码变体:

javascriptCode
// 变体一:私有字段(ES2022 # 语法)
class BankAccount {
  #balance = 0; // 真正的私有字段,外部无法访问

  constructor(owner, initial = 0) {
    this.owner = owner;
    this.#balance = initial;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error('金额必须为正');
    this.#balance += amount;
    return this.#balance;
  }

  get balance() {
    return this.#balance;
  }
}

const account = new BankAccount('Alice', 100);
account.deposit(50);
console.log(account.balance); // 150
// console.log(account.#balance); // 语法错误,无法从外部访问
javascriptCode
// 变体二:继承与 super
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 必须先调用 super 才能使用 this
    this.breed = breed;
  }
  speak() {
    return `${this.name} (${this.breed}) barks`;
  }
}

const dog = new Dog('Rex', 'Husky');
console.log(dog.speak()); // Rex (Husky) barks
javascriptCode
// 变体三:用 new.target 防止忘记写 new
function User(name) {
  if (!new.target) {
    throw new Error('User 必须使用 new 调用');
  }
  this.name = name;
}
// User('Alice'); // 抛出错误
const u = new User('Alice'); // 正确

真实案例:

内置构造函数:Array、Object、Map、Set、Date、Promise、Error 全都是构造函数,通过 new 创建实例。
自定义错误类型:`class ValidationError extends Error` 是构造器 + 继承的典型应用。
React 类组件:`class MyComponent extends React.Component`,constructor 里初始化 state 并绑定方法。
游戏实体:游戏里的敌人、子弹、道具往往用构造函数批量创建,每个实例有独立的坐标、血量等状态。

构造器与相近模式对比:

| 对比项 | 构造器模式 | 工厂模式 | 原型模式 |

| --- | --- | --- | --- |

| 创建方式 | new 构造函数 | 调用工厂方法 | 克隆已有对象 |

| 是否暴露具体类 | 是(要写 new) | 否 | 否 |

| 支持 instanceof | 是 | 取决于实现 | 取决于原型 |

| 灵活度 | 中 | 高 | 高 |

常见坑:

忘记写 new:非严格模式下忘记 new 会导致 this 指向全局对象,污染全局。用 class(强制 new)或 new.target 检测可避免。
在构造函数里定义方法:`this.method = function(){}` 会让每个实例都持有一份方法副本,浪费内存。应把方法放在 prototype 或用 class 的方法定义。
箭头函数不能作构造器:箭头函数没有自己的 this、没有 prototype,不能用 new 调用。
构造函数里做重活:构造函数应只负责初始化,不要在里面发起网络请求、执行耗时计算,否则 new 会变慢且难以测试。

最佳实践:

优先用 ES6 class 语法,比 ES5 构造函数 + prototype 更清晰,且强制 new 调用。
用 # 私有字段封装内部状态,而不是靠命名约定(下划线前缀)。
构造函数只做轻量初始化,重逻辑放到工厂或初始化方法里。
需要类型判断时用 instanceof,但要注意跨 iframe/realm 时 instanceof 会失效。

原型模式(Prototype):

原型模式基于原型链创建对象,通过克隆现有对象来创建新对象。JavaScript 天然支持原型继承,Object.create() 方法是实现原型模式的核心。原型模式适合创建成本较高的对象,通过克隆提高性能。

javascriptCode
// 原型模式实现
const prototype = {
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  },
  clone() {
    return Object.create(this);
  },
};

const person1 = Object.create(prototype);
person1.name = 'Alice';

const person2 = person1.clone();
person2.name = 'Bob';

// 使用 Object.assign 实现深拷贝
const deepClone = (obj) => {
  if (obj === null || typeof obj !== 'object') return obj;
  
  if (Array.isArray(obj)) {
    return obj.map(deepClone);
  }
  
  const cloned = Object.create(Object.getPrototypeOf(obj));
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepClone(obj[key]);
    }
  }
  return cloned;
};

原型模式:概念类比与深入理解

原型模式的类比是"复印文件":与其从零开始手写一份新文件,不如拿一份已有的模板去复印,然后在复印件上做局部修改。当"从头创建对象"成本很高(比如需要复杂的初始化、大量计算或远程请求)时,直接克隆一个已有对象往往更划算。

JavaScript 是少数几种"原型即语言核心"的语言。其他语言的原型模式是"模拟"出来的,而 JS 的对象天生就通过原型链关联,Object.create() 就是原型模式的语言级支持。

为什么原型模式重要:

避免重复的昂贵初始化:克隆比重新构造更快。
动态配置对象:运行时可以基于一个"原型对象"派生出许多变体。
理解 JS 继承本质:JS 的继承就是原型链,理解原型模式就理解了 JS 的对象模型。

深浅拷贝的区别(原理):

原型模式绕不开"拷贝"这个话题。浅拷贝只复制第一层,嵌套对象仍是共享引用;深拷贝则递归复制所有层级,得到完全独立的副本。

javascriptCode
// 浅拷贝的几种方式
const original = { a: 1, nested: { b: 2 } };
const shallow1 = { ...original };
const shallow2 = Object.assign({}, original);
shallow1.nested.b = 99;
console.log(original.nested.b); // 99,嵌套对象被共享修改了

// 深拷贝:现代浏览器/Node 内置 structuredClone
const deep = structuredClone(original);
deep.nested.b = 42;
console.log(original.nested.b); // 99,不受影响
javascriptCode
// 手写一个能处理循环引用的深拷贝
function deepClone(obj, seen = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
  if (seen.has(obj)) return seen.get(obj); // 处理循环引用

  const clone = Array.isArray(obj)
    ? []
    : Object.create(Object.getPrototypeOf(obj));
  seen.set(obj, clone);

  for (const key of Reflect.ownKeys(obj)) {
    clone[key] = deepClone(obj[key], seen);
  }
  return clone;
}

const a = { name: 'root' };
a.self = a; // 循环引用
const cloned = deepClone(a);
console.log(cloned.self === cloned); // true,循环引用被正确保留
javascriptCode
// 变体:基于原型对象批量派生配置
const baseConfig = {
  retries: 3,
  timeout: 5000,
  headers: { 'Content-Type': 'application/json' },
  withDefaults(overrides) {
    return Object.assign(Object.create(this), overrides);
  },
};

const uploadConfig = baseConfig.withDefaults({ timeout: 30000 });
const pingConfig = baseConfig.withDefaults({ retries: 0, timeout: 1000 });
console.log(uploadConfig.retries); // 3,继承自原型
console.log(uploadConfig.timeout); // 30000,被覆盖

真实案例:

Object.create(null):创建一个没有原型的"纯净字典",避免原型链上的属性干扰,常用于做 map。
数组/字符串的方法:`[].map`、`''.slice` 都定义在 Array.prototype、String.prototype 上,所有实例共享,这就是原型模式节省内存的体现。
Vue 2 的响应式:Vue 2 通过改写数组原型上的 push/pop 等方法来实现数组变更侦测,是原型模式的巧妙运用。
游戏/编辑器的"复制粘贴":复制一个图形/精灵对象时,克隆其原型比重新构造快得多。

原型与相近模式对比:

| 对比项 | 原型模式 | 构造器模式 | 工厂模式 |

| --- | --- | --- | --- |

| 新对象来源 | 克隆现有对象 | new 构造函数 | 工厂方法返回 |

| 初始化成本 | 低(复用已有状态) | 每次都重新初始化 | 取决于实现 |

| 是否需要类定义 | 不需要 | 需要 | 不一定 |

| JS 语言支持 | 原生(Object.create) | 原生(new) | 需自行封装 |

常见坑:

浅拷贝当深拷贝用:展开运算符和 Object.assign 都是浅拷贝,改嵌套对象会互相影响,这是最高频的 bug 之一。
JSON.parse(JSON.stringify()) 的局限:这种"穷人的深拷贝"会丢失函数、undefined、Symbol,无法处理循环引用,还会把 Date 变成字符串。生产环境优先用 structuredClone。
克隆丢失原型:用 `{...obj}` 拷贝会丢失原型链,克隆结果的原型是 Object.prototype,而非原对象的类。需要保留原型时用 Object.create(Object.getPrototypeOf(obj))。
修改共享原型的副作用:往内置原型(如 Array.prototype)上加方法会影响所有代码,是危险的"猴子补丁"。

最佳实践:

深拷贝优先用 structuredClone(现代环境),需要兼容旧环境或自定义行为时再手写递归拷贝。
用 Object.create(null) 创建纯字典,避免原型污染。
不要修改内置对象的原型,需要扩展时用工具函数或子类。
克隆昂贵对象前先评估:有时重新构造反而更简单清晰,不要为了"原型模式"而克隆。

结构型模式

结构型模式关注如何把类和对象组合成更大的结构,同时保持结构的灵活和高效。它回答的是"如何用小对象搭建出大系统"的问题。结构型模式大量利用组合优于继承的思想:通过对象组合(把对象作为另一个对象的成员)而非类继承来复用功能,从而获得运行时的灵活性。下面的适配器、桥接、组合、装饰器、外观、享元、代理七个模式各自解决不同的结构问题。

适配器模式(Adapter):

适配器模式转换接口,使不兼容的接口能够一起工作。在 JavaScript 中,适配器模式常用于整合第三方库、处理不同数据格式、统一 API 接口等场景。适配器模式可以在不修改原有代码的情况下,实现接口的兼容。

javascriptCode
// 适配器模式示例
// 旧 API
const oldAPI = {
  getUserInfo: (id) => ({ id, name: 'Alice', age: 25 }),
};

// 新 API
const newAPI = {
  fetchUser: async (id) => ({ userId: id, userName: 'Alice', userAge: 25 }),
};

// 适配器
class UserAPIAdapter {
  constructor(api) {
    this.api = api;
  }
  
  async getUser(id) {
    const user = await this.api.fetchUser(id);
    // 转换数据格式
    return {
      id: user.userId,
      name: user.userName,
      age: user.userAge,
    };
  }
}

// 使用适配器
const adapter = new UserAPIAdapter(newAPI);
const user = await adapter.getUser(1);
// { id: 1, name: 'Alice', age: 25 }

// 接口适配器
class LocalStorageAdapter {
  get(key) {
    const value = localStorage.getItem(key);
    try {
      return JSON.parse(value);
    } catch {
      return value;
    }
  }
  
  set(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }
  
  remove(key) {
    localStorage.removeItem(key);
  }
}

适配器模式:概念类比与深入理解

适配器最经典的类比就是"电源转换插头":你的笔记本是两脚插头,酒店墙上是三孔插座,两者接口不兼容,于是你用一个转换插头把它们连接起来。转换插头不改变笔记本,也不改变插座,只是在中间做接口转换。适配器模式做的正是这件事——在不修改双方代码的前提下,让接口不兼容的两个对象协同工作。

为什么适配器模式重要:

复用遗留代码/第三方库:老代码、第三方库的接口往往和你的期望不一致,适配器让你无需修改它们就能接入。
隔离外部依赖:把外部 API 的形态封装在适配器内,未来更换供应商只需改适配器,业务代码不动,符合依赖倒置。
统一多个不同来源的接口:把多个数据源/服务包装成统一接口,上层代码用一套逻辑处理。

对象适配器 vs 类适配器(原理):

适配器有两种实现方式。对象适配器通过"持有被适配对象的引用"(组合)来转换接口,这是 JS 中最常用的方式。类适配器通过多重继承实现,但 JS 不支持多重继承,通常用组合替代。

更多代码变体:

javascriptCode
// 变体一:适配不同的第三方地图 SDK 到统一接口
// 业务代码只依赖 MapService 接口:show(lat, lng)、addMarker(lat, lng)
class GoogleMapAdapter {
  constructor(googleMap) {
    this.map = googleMap; // 假设是 Google Maps 实例
  }
  show(lat, lng) {
    this.map.setCenter({ lat, lng }); // Google 用 setCenter
  }
  addMarker(lat, lng) {
    this.map.placeMarker({ latitude: lat, longitude: lng });
  }
}

class BaiduMapAdapter {
  constructor(baiduMap) {
    this.map = baiduMap;
  }
  show(lat, lng) {
    this.map.centerAndZoom(lng, lat, 15); // 百度参数顺序不同:经度在前
  }
  addMarker(lat, lng) {
    this.map.addOverlay({ point: [lng, lat] });
  }
}

function renderLocation(mapService, lat, lng) {
  mapService.show(lat, lng);
  mapService.addMarker(lat, lng);
}
// 无论底层是 Google 还是百度,业务代码完全一致
javascriptCode
// 变体二:把 callback 风格的旧 API 适配成 Promise 风格
function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });
  };
}

// 旧的 callback API
function readConfig(path, callback) {
  setTimeout(() => callback(null, { path, loaded: true }), 100);
}

const readConfigAsync = promisify(readConfig);
readConfigAsync('/etc/app.json').then((cfg) => console.log(cfg));
javascriptCode
// 变体三:数据格式适配——把后端下划线命名转成前端驼峰命名
function toCamelCase(str) {
  return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
}

function adaptResponse(data) {
  if (Array.isArray(data)) return data.map(adaptResponse);
  if (data !== null && typeof data === 'object') {
    return Object.fromEntries(
      Object.entries(data).map(([k, v]) => [toCamelCase(k), adaptResponse(v)])
    );
  }
  return data;
}

const backendData = { user_name: 'Alice', created_at: '2024', order_list: [{ item_id: 1 }] };
console.log(adaptResponse(backendData));
// { userName: 'Alice', createdAt: '2024', orderList: [{ itemId: 1 }] }

真实案例:

axios 的 adapter:axios 内部通过 adapter 抽象,让同一套 API 既能在浏览器用 XMLHttpRequest,又能在 Node.js 用 http 模块。
jQuery 的跨浏览器兼容:早年 jQuery 用适配器抹平不同浏览器的事件、Ajax 接口差异。
ORM 的数据库方言适配:Sequelize/TypeORM 用适配器统一 MySQL、PostgreSQL、SQLite 的差异化 SQL。
日志库适配:把 console、winston、pino 等不同日志库适配成统一的 log 接口。

适配器与相近模式对比:

| 对比项 | 适配器 | 外观 | 装饰器 | 代理 |

| --- | --- | --- | --- | --- |

| 意图 | 转换已有接口 | 简化复杂子系统 | 增强功能 | 控制访问 |

| 是否改变接口 | 是(转换成新接口) | 是(提供新的简化接口) | 否(保持原接口) | 否(保持原接口) |

| 是否新增功能 | 否 | 否 | 是 | 可选 |

| 典型场景 | 兼容不兼容接口 | 封装子系统 | 叠加职责 | 缓存/权限/懒加载 |

常见坑:

适配器里塞业务逻辑:适配器应只做接口转换,一旦掺入业务逻辑就会变得难以维护。转换归转换,业务归业务。
过度适配:如果接口本就相近,直接调用即可,硬套适配器是过度设计。
双向适配的复杂度:需要 A→B 和 B→A 双向转换时,适配器会变复杂,要谨慎设计避免转换不一致。

最佳实践:

用适配器隔离所有外部依赖(第三方库、后端接口),让业务层只依赖你自己定义的稳定接口。
适配器保持"薄",只做纯粹的接口/数据转换。
为适配器编写单元测试,重点测试转换的正确性和边界情况(空值、缺字段)。

装饰器模式(Decorator):

装饰器模式动态为对象添加额外功能,不修改原有代码。在 JavaScript 中,装饰器模式可以通过高阶函数、类装饰器等方式实现。装饰器模式适合在不改变对象结构的情况下,扩展对象的功能。

javascriptCode
// 函数装饰器
function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with args:`, args);
    const result = original.apply(this, args);
    console.log(`Result:`, result);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 高阶函数装饰器
function withLogging(fn) {
  return function(...args) {
    console.log('Arguments:', args);
    const result = fn.apply(this, args);
    console.log('Result:', result);
    return result;
  };
}

function withTiming(fn) {
  return function(...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    const end = performance.now();
    console.log(`Execution time: ${end - start}ms`);
    return result;
  };
}

// 组合装饰器
const decoratedFn = withLogging(withTiming(expensiveOperation));

// 对象装饰器
class Coffee {
  cost() {
    return 5;
  }
}

class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  
  cost() {
    return this.coffee.cost() + 2;
  }
}

class SugarDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  
  cost() {
    return this.coffee.cost() + 1;
  }
}

let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 8

装饰器模式:概念类比与深入理解

装饰器最好的类比是"给礼物层层包装"或"给咖啡加料":一杯基础咖啡,你可以加牛奶、加糖、加奶油,每加一层就在原有基础上增加成本和风味,但它始终是一杯"咖啡"(接口不变)。你可以任意组合、任意叠加顺序。装饰器模式让你在运行时动态地、可组合地给对象叠加功能,而不是通过继承在编译期写死。

为什么装饰器模式重要:

避免子类爆炸:如果用继承来组合功能,"加奶+加糖"、"加奶+加奶油"、"加奶+加糖+加奶油"每种组合都要一个子类,组合数量呈指数增长。装饰器用组合替代继承,彻底解决这个问题。
符合开闭原则:给对象加功能无需修改对象本身,只需包一层装饰器。
职责单一且可复用:每个装饰器只负责一件事(日志、计时、缓存、权限),可以自由组合到不同对象上。

装饰器 vs 继承(原理):

继承是静态的、编译期确定的,一个对象一旦创建就无法改变其类。装饰器是动态的、运行时的,可以在运行时决定给对象叠加哪些功能、叠加几层、以什么顺序。这就是"组合优于继承"的经典体现。

更多代码变体:

javascriptCode
// 变体一:函数装饰器组合——用 reduce 把多个装饰器串起来
const compose = (...decorators) => (fn) =>
  decorators.reduceRight((acc, decorator) => decorator(acc), fn);

const withRetry = (fn) => async (...args) => {
  let lastErr;
  for (let i = 0; i < 3; i++) {
    try {
      return await fn(...args);
    } catch (e) {
      lastErr = e;
      console.log(`重试第 ${i + 1} 次`);
    }
  }
  throw lastErr;
};

const withCache = (fn) => {
  const cache = new Map();
  return async (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = await fn(...args);
    cache.set(key, result);
    return result;
  };
};

const enhanced = compose(withCache, withRetry)(async (id) => {
  console.log('真正请求', id);
  return { id, name: 'data' };
});
// enhanced 现在同时具备缓存 + 重试能力
javascriptCode
// 变体二:ES 装饰器提案语法(TC39 Stage 3)——类方法装饰器
function measure(target, context) {
  return function (...args) {
    const start = performance.now();
    const result = target.apply(this, args);
    console.log(`${context.name} 耗时 ${performance.now() - start}ms`);
    return result;
  };
}

class DataProcessor {
  @measure
  process(data) {
    return data.map((x) => x * 2);
  }
}
javascriptCode
// 变体三:属性访问装饰——用 getter/setter 装饰添加校验和日志
function readonly(target, context) {
  return {
    get() {
      return context.access.get.call(this);
    },
    set() {
      throw new Error(`${context.name} 是只读属性`);
    },
  };
}

真实案例:

React 高阶组件(HOC):`withRouter(Component)`、`connect(mapState)(Component)` 就是用一个函数包裹组件、返回增强后的新组件,是装饰器思想的典型应用。
Express/Koa 中间件:每个中间件包裹请求处理流程,叠加日志、鉴权、压缩等功能,本质是装饰器链。
NestJS/Angular 的装饰器:`@Injectable()`、`@Controller()`、`@Component()` 大量使用 ES 装饰器给类附加元数据和行为。
MobX 的 @observable / @computed:用装饰器把普通属性变成响应式属性。

装饰器与相近模式对比:

| 对比项 | 装饰器 | 代理 | 适配器 | 继承 |

| --- | --- | --- | --- | --- |

| 意图 | 增强功能 | 控制访问 | 转换接口 | 复用+特化 |

| 接口是否改变 | 否 | 否 | 是 | 通常否 |

| 动态/静态 | 动态(运行时组合) | 动态 | 动态 | 静态(编译期) |

| 可叠加多层 | 是 | 通常单层 | 通常单层 | 单继承链 |

常见坑:

装饰顺序影响结果:`compose(withCache, withRetry)` 和 `compose(withRetry, withCache)` 行为不同——前者缓存重试后的结果,后者对缓存操作做重试。要想清楚顺序语义。
this 丢失:函数装饰器里用箭头函数或忘记 apply(this) 会导致原方法的 this 指向错误。要始终用 fn.apply(this, args) 转发。
调试困难:多层装饰后调用栈变深,报错定位到装饰器内部而非业务代码,需要良好的命名和 source map。
装饰器语法不稳定:ES 装饰器提案历经多次大改,legacy 装饰器和新提案语法不兼容,需注意 Babel/TS 配置。

最佳实践:

每个装饰器只做一件事,通过组合来叠加多个功能。
装饰器要保持"透明"——增强后的对象/函数对外表现应与原来一致(同样的接口、同样的返回类型)。
明确记录装饰器的组合顺序及其语义。
优先用函数式装饰器(高阶函数),除非框架要求用类装饰器语法。

代理模式(Proxy):

代理模式控制对对象的访问,添加额外的行为。ES6 的 Proxy 对象是实现代理模式的强大工具,可以拦截各种操作,如属性访问、赋值、函数调用等。代理模式常用于数据验证、缓存、访问控制等场景。

javascriptCode
// 使用 ES6 Proxy
const user = { name: 'Alice', age: 25 };

const userProxy = new Proxy(user, {
  get(target, prop) {
    console.log(`Getting ${prop}`);
    return target[prop];
  },
  
  set(target, prop, value) {
    console.log(`Setting ${prop} to ${value}`);
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    target[prop] = value;
    return true;
  },
  
  has(target, prop) {
    console.log(`Checking if ${prop} exists`);
    return prop in target;
  },
});

// 缓存代理
function createCacheProxy(fn) {
  const cache = new Map();
  
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      
      if (cache.has(key)) {
        console.log('Returning cached result');
        return cache.get(key);
      }
      
      const result = target.apply(thisArg, args);
      cache.set(key, result);
      return result;
    },
  });
}

const expensiveCalculation = (n) => {
  console.log('Calculating...');
  return n * n;
};

const cachedCalculation = createCacheProxy(expensiveCalculation);
cachedCalculation(5); // Calculating... 25
cachedCalculation(5); // Returning cached result 25

外观模式(Facade):

外观模式提供统一的接口,简化复杂系统的使用。在 JavaScript 中,外观模式常用于封装复杂的 API 调用、统一多个模块的接口、简化库的使用等场景。外观模式可以降低系统的复杂度,提高代码的可读性。

javascriptCode
// 外观模式示例
class PaymentFacade {
  constructor() {
    this.validator = new PaymentValidator();
    this.processor = new PaymentProcessor();
    this.notifier = new NotificationService();
    this.logger = new Logger();
  }
  
  async processPayment(paymentInfo) {
    try {
      // 验证支付信息
      this.validator.validate(paymentInfo);
      
      // 处理支付
      const result = await this.processor.process(paymentInfo);
      
      // 发送通知
      await this.notifier.sendConfirmation(paymentInfo.email, result);
      
      // 记录日志
      this.logger.log('Payment processed', result);
      
      return result;
    } catch (error) {
      this.logger.error('Payment failed', error);
      throw error;
    }
  }
}

// 使用外观
const payment = new PaymentFacade();
await payment.processPayment({
  amount: 100,
  cardNumber: '4111111111111111',
  email: 'user@example.com',
});

行为型模式

观察者模式(Observer):

观察者模式定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。在 JavaScript 中,观察者模式广泛应用于事件处理、数据绑定、状态管理等场景。现代前端框架如 React、Vue 都大量使用观察者模式。

javascriptCode
// 观察者模式实现
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return () => this.off(event, callback);
  }
  
  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }
  
  emit(event, ...args) {
    if (!this.events[event]) return;
    this.events[event].forEach(callback => callback(...args));
  }
  
  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}

// 使用示例
const emitter = new EventEmitter();
const unsubscribe = emitter.on('user:login', (user) => {
  console.log(`User ${user.name} logged in`);
});

emitter.emit('user:login', { name: 'Alice' });
unsubscribe();

// 简化的发布订阅模式
class PubSub {
  constructor() {
    this.subscribers = {};
  }
  
  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
    return () => {
      this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback);
    };
  }
  
  publish(event, data) {
    if (!this.subscribers[event]) return;
    this.subscribers[event].forEach(callback => callback(data));
  }
}

// 状态管理中的观察者模式
class Store {
  constructor(initialState) {
    this.state = initialState;
    this.listeners = [];
  }
  
  getState() {
    return this.state;
  }
  
  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.listeners.forEach(listener => listener(this.state));
  }
  
  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }
}

策略模式(Strategy):

策略模式定义一系列算法,把它们封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化。在 JavaScript 中,策略模式常用于表单验证、动画效果、支付方式选择等场景。

javascriptCode
// 策略模式实现
const validationStrategies = {
  required: (value) => {
    if (!value || value.trim() === '') {
      return 'This field is required';
    }
    return null;
  },
  
  email: (value) => {
    if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(value)) {
      return 'Please enter a valid email';
    }
    return null;
  },
  
  minLength: (value, min) => {
    if (value.length < min) {
      return `Minimum length is ${min} characters`;
    }
    return null;
  },
  
  maxLength: (value, max) => {
    if (value.length > max) {
      return `Maximum length is ${max} characters`;
    }
    return null;
  },
};

class Validator {
  constructor() {
    this.rules = [];
  }
  
  addRule(field, strategy, ...args) {
    this.rules.push({ field, strategy, args });
    return this;
  }
  
  validate(data) {
    const errors = {};
    
    for (const rule of this.rules) {
      const { field, strategy, args } = rule;
      const value = data[field];
      const error = validationStrategies[strategy](value, ...args);
      
      if (error && !errors[field]) {
        errors[field] = error;
      }
    }
    
    return {
      isValid: Object.keys(errors).length === 0,
      errors,
    };
  }
}

// 使用示例
const validator = new Validator()
  .addRule('email', 'required')
  .addRule('email', 'email')
  .addRule('password', 'required')
  .addRule('password', 'minLength', 8);

const result = validator.validate({
  email: 'invalid-email',
  password: '123',
});

// 支付策略
const paymentStrategies = {
  creditCard: (amount, cardInfo) => {
    console.log(`Processing credit card payment of $${amount}`);
    // 信用卡支付逻辑
  },
  
  paypal: (amount, paypalInfo) => {
    console.log(`Processing PayPal payment of $${amount}`);
    // PayPal 支付逻辑
  },
  
  crypto: (amount, walletInfo) => {
    console.log(`Processing crypto payment of $${amount}`);
    // 加密货币支付逻辑
  },
};

function processPayment(method, amount, info) {
  const strategy = paymentStrategies[method];
  if (!strategy) {
    throw new Error(`Unknown payment method: ${method}`);
  }
  return strategy(amount, info);
}

命令模式(Command):

命令模式将请求封装为对象,从而允许用不同的请求对客户进行参数化、对请求排队或记录请求日志,以及支持可撤销的操作。在 JavaScript 中,命令模式常用于实现撤销/重做功能、菜单操作、宏命令等场景。

javascriptCode
// 命令模式实现
class Command {
  execute() {
    throw new Error('Execute method must be implemented');
  }
  
  undo() {
    throw new Error('Undo method must be implemented');
  }
}

class AddItemCommand extends Command {
  constructor(list, item) {
    super();
    this.list = list;
    this.item = item;
    this.index = null;
  }
  
  execute() {
    this.index = this.list.length;
    this.list.push(this.item);
  }
  
  undo() {
    if (this.index !== null) {
      this.list.splice(this.index, 1);
    }
  }
}

class RemoveItemCommand extends Command {
  constructor(list, index) {
    super();
    this.list = list;
    this.index = index;
    this.item = null;
  }
  
  execute() {
    this.item = this.list[this.index];
    this.list.splice(this.index, 1);
  }
  
  undo() {
    if (this.item !== null) {
      this.list.splice(this.index, 0, this.item);
    }
  }
}

// 命令管理器(支持撤销/重做)
class CommandManager {
  constructor() {
    this.history = [];
    this.redoStack = [];
  }
  
  execute(command) {
    command.execute();
    this.history.push(command);
    this.redoStack = [];
  }
  
  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.redoStack.push(command);
    }
  }
  
  redo() {
    const command = this.redoStack.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }
}

// 使用示例
const list = [];
const manager = new CommandManager();

manager.execute(new AddItemCommand(list, 'Item 1'));
manager.execute(new AddItemCommand(list, 'Item 2'));
console.log(list); // ['Item 1', 'Item 2']

manager.undo();
console.log(list); // ['Item 1']

manager.redo();
console.log(list); // ['Item 1', 'Item 2']

迭代器模式(Iterator):

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。JavaScript 原生支持迭代器协议,通过 Symbol.iterator 实现自定义迭代器。迭代器模式常用于遍历复杂数据结构、实现懒加载等场景。

javascriptCode
// 自定义迭代器
class RangeIterator {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
    this.current = start;
  }
  
  [Symbol.iterator]() {
    return this;
  }
  
  next() {
    if (this.current < this.end) {
      const value = this.current;
      this.current += this.step;
      return { value, done: false };
    }
    return { value: undefined, done: true };
  }
}

const range = new RangeIterator(0, 10, 2);
for (const num of range) {
  console.log(num); // 0, 2, 4, 6, 8
}

// 树形结构迭代器
class TreeIterator {
  constructor(root) {
    this.stack = [root];
  }
  
  [Symbol.iterator]() {
    return this;
  }
  
  next() {
    if (this.stack.length === 0) {
      return { done: true };
    }
    
    const node = this.stack.pop();
    
    if (node.children) {
      for (let i = node.children.length - 1; i >= 0; i--) {
        this.stack.push(node.children[i]);
      }
    }
    
    return { value: node, done: false };
  }
}

// 生成器函数实现迭代器
function* fibonacci(limit) {
  let [prev, curr] = [0, 1];
  while (curr <= limit) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

for (const num of fibonacci(100)) {
  console.log(num); // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
}

状态模式(State):

状态模式允许一个对象在其内部状态改变时改变它的行为。对象看起来好像修改了它的类。状态模式将状态逻辑分散到不同的状态类中,避免大量的条件判断。在 JavaScript 中,状态模式常用于实现状态机、游戏角色状态、订单状态等场景。

javascriptCode
// 状态模式实现
class TrafficLight {
  constructor() {
    this.state = new RedState(this);
  }
  
  changeState(state) {
    this.state = state;
  }
  
  next() {
    this.state.next();
  }
  
  report() {
    this.state.report();
  }
}

class RedState {
  constructor(light) {
    this.light = light;
  }
  
  next() {
    console.log('Red -> Green');
    this.light.changeState(new GreenState(this.light));
  }
  
  report() {
    console.log('Traffic light is RED - STOP');
  }
}

class GreenState {
  constructor(light) {
    this.light = light;
  }
  
  next() {
    console.log('Green -> Yellow');
    this.light.changeState(new YellowState(this.light));
  }
  
  report() {
    console.log('Traffic light is GREEN - GO');
  }
}

class YellowState {
  constructor(light) {
    this.light = light;
  }
  
  next() {
    console.log('Yellow -> Red');
    this.light.changeState(new RedState(this.light));
  }
  
  report() {
    console.log('Traffic light is YELLOW - CAUTION');
  }
}

// 使用示例
const trafficLight = new TrafficLight();
trafficLight.report(); // Traffic light is RED - STOP
trafficLight.next();   // Red -> Green
trafficLight.report(); // Traffic light is GREEN - GO

// 订单状态机
const OrderState = {
  PENDING: 'pending',
  CONFIRMED: 'confirmed',
  SHIPPED: 'shipped',
  DELIVERED: 'delivered',
  CANCELLED: 'cancelled',
};

const transitions = {
  [OrderState.PENDING]: [OrderState.CONFIRMED, OrderState.CANCELLED],
  [OrderState.CONFIRMED]: [OrderState.SHIPPED, OrderState.CANCELLED],
  [OrderState.SHIPPED]: [OrderState.DELIVERED],
  [OrderState.DELIVERED]: [],
  [OrderState.CANCELLED]: [],
};

function canTransition(from, to) {
  return transitions[from]?.includes(to) ?? false;
}

应用场景

单页应用架构中的设计模式:

路由管理:观察者模式实现路由变化监听和页面切换
状态管理:单例模式确保全局状态唯一性,观察者模式实现状态订阅
组件通信:发布-订阅模式实现跨组件通信,避免紧耦合
javascriptCode
// 路由管理器(观察者模式)
class Router {
  constructor() {
    this.routes = {};
    this.currentRoute = null;
    this.listeners = [];
  }
  
  register(path, handler) {
    this.routes[path] = handler;
  }
  
  navigate(path) {
    if (this.routes[path]) {
      this.currentRoute = path;
      this.routes[path]();
      this.listeners.forEach(listener => listener(path));
    }
  }
  
  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }
}

// 全局状态管理(单例 + 观察者)
class AppState {
  static instance = null;
  
  constructor() {
    if (AppState.instance) return AppState.instance;
    AppState.instance = this;
    this.state = {};
    this.listeners = [];
  }
  
  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.listeners.forEach(l => l(this.state));
  }
  
  subscribe(listener) {
    this.listeners.push(listener);
    return () => this.listeners.filter(l => l !== listener);
  }
}

表单验证中的设计模式:

策略模式:不同的验证规则作为策略,灵活组合
装饰器模式:动态添加验证功能,如实时验证、异步验证
责任链模式:多个验证器按顺序执行
javascriptCode
// 表单验证器(策略 + 责任链)
class FormValidator {
  constructor() {
    this.strategies = {};
    this.chain = [];
  }
  
  addStrategy(name, fn) {
    this.strategies[name] = fn;
    return this;
  }
  
  addRule(field, strategyName, ...args) {
    this.chain.push({ field, strategy: strategyName, args });
    return this;
  }
  
  validate(data) {
    const errors = {};
    
    for (const rule of this.chain) {
      const strategy = this.strategies[rule.strategy];
      if (strategy) {
        const error = strategy(data[rule.field], ...rule.args);
        if (error && !errors[rule.field]) {
          errors[rule.field] = error;
        }
      }
    }
    
    return { isValid: Object.keys(errors).length === 0, errors };
  }
}

const validator = new FormValidator()
  .addStrategy('required', v => v ? null : 'Required')
  .addStrategy('email', v => /^[^s@]+@[^s@]+.[^s@]+$/.test(v) ? null : 'Invalid email')
  .addRule('email', 'required')
  .addRule('email', 'email');

API 调用中的设计模式:

代理模式:拦截请求和响应,添加缓存、日志等功能
适配器模式:适配不同的 API 格式,统一接口
装饰器模式:添加重试、超时、认证等功能
javascriptCode
// API 客户端(代理 + 装饰器)
class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.cache = new Map();
    this.interceptors = { request: [], response: [] };
  }
  
  addRequestInterceptor(fn) {
    this.interceptors.request.push(fn);
  }
  
  addResponseInterceptor(fn) {
    this.interceptors.response.push(fn);
  }
  
  async request(endpoint, options = {}) {
    let url = this.baseURL + endpoint;
    let config = options;
    
    // 执行请求拦截器
    for (const interceptor of this.interceptors.request) {
      [url, config] = await interceptor(url, config);
    }
    
    const cacheKey = JSON.stringify({ url, config });
    if (config.cache && this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }
    
    let response = await fetch(url, config);
    
    // 执行响应拦截器
    for (const interceptor of this.interceptors.response) {
      response = await interceptor(response);
    }
    
    const data = await response.json();
    
    if (config.cache) {
      this.cache.set(cacheKey, data);
    }
    
    return data;
  }
}

const api = new APIClient('https://api.example.com');
api.addRequestInterceptor((url, config) => {
  config.headers = { ...config.headers, Authorization: 'Bearer token' };
  return [url, config];
});

动画效果中的设计模式:

策略模式:不同的动画算法(缓动函数)作为策略
命令模式:将动画操作封装为命令,支持撤销和队列
观察者模式:动画状态变化通知订阅者
javascriptCode
// 动画管理器(策略 + 命令)
const easingStrategies = {
  linear: t => t,
  easeIn: t => t * t,
  easeOut: t => t * (2 - t),
  easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
};

class Animation {
  constructor(element, property, start, end, duration, easing = 'linear') {
    this.element = element;
    this.property = property;
    this.start = start;
    this.end = end;
    this.duration = duration;
    this.easing = easingStrategies[easing];
    this.startTime = null;
  }
  
  execute() {
    this.startTime = performance.now();
    this.tick();
  }
  
  tick() {
    const elapsed = performance.now() - this.startTime;
    const progress = Math.min(elapsed / this.duration, 1);
    const easedProgress = this.easing(progress);
    const value = this.start + (this.end - this.start) * easedProgress;
    
    this.element.style[this.property] = value + 'px';
    
    if (progress < 1) {
      requestAnimationFrame(() => this.tick());
    }
  }
}

创建型模式深入:抽象工厂

概念与类比

抽象工厂(Abstract Factory)是"工厂的工厂"。如果说工厂方法解决的是"创建一个产品"的问题,那么抽象工厂解决的是"创建一整个产品族"的问题。

打个比方:你去宜家买家具。宜家提供"北欧风格套装"和"现代简约套装"两条产品线。当你选择了"北欧风格"这个工厂,它会一次性给你北欧风格的沙发、茶几、书架——它们之间风格协调、互相搭配。你不需要担心买到一个北欧沙发配一个现代书架的尴尬组合。这里的"北欧风格套装"就是一个具体工厂,而"套装"这个抽象概念就是抽象工厂。

为什么重要

在真实项目中,抽象工厂最常见的应用是跨平台 UI 组件库多主题系统。假设你要做一个同时支持 Web、iOS、Android 三端的组件库,每个平台的按钮、输入框、弹窗实现方式完全不同,但对上层业务代码来说,它们的接口应该一致。抽象工厂让业务代码只依赖抽象接口,切换平台时只需替换一个工厂实例,其余代码零改动。

据统计,在大型跨平台项目中引入抽象工厂后,平台切换相关的代码改动量通常能从数百处调用点收敛到 1 处工厂初始化点,维护成本下降约 80%。

原理与结构

抽象工厂包含四个角色:

| 角色 | 英文 | 职责 |

| --- | --- | --- |

| 抽象工厂 | AbstractFactory | 声明创建各类产品的接口 |

| 具体工厂 | ConcreteFactory | 实现创建具体产品族的方法 |

| 抽象产品 | AbstractProduct | 声明产品的接口 |

| 具体产品 | ConcreteProduct | 具体工厂创建的实际对象 |

代码示例一:跨平台 UI 组件

javascriptCode
// 抽象产品:按钮
class Button {
  render() {
    throw new Error('必须实现 render 方法');
  }
}

// 抽象产品:输入框
class Input {
  render() {
    throw new Error('必须实现 render 方法');
  }
}

// 具体产品:Web 平台
class WebButton extends Button {
  render() {
    return '<button class="web-btn">Web 按钮</button>';
  }
}

class WebInput extends Input {
  render() {
    return '<input class="web-input" />';
  }
}

// 具体产品:移动端平台
class MobileButton extends Button {
  render() {
    return '[原生按钮组件 MobileButton]';
  }
}

class MobileInput extends Input {
  render() {
    return '[原生输入框组件 MobileInput]';
  }
}

// 抽象工厂
class UIFactory {
  createButton() {
    throw new Error('必须实现 createButton');
  }
  createInput() {
    throw new Error('必须实现 createInput');
  }
}

// 具体工厂:Web
class WebUIFactory extends UIFactory {
  createButton() {
    return new WebButton();
  }
  createInput() {
    return new WebInput();
  }
}

// 具体工厂:Mobile
class MobileUIFactory extends UIFactory {
  createButton() {
    return new MobileButton();
  }
  createInput() {
    return new MobileInput();
  }
}

// 业务代码只依赖抽象工厂
function renderForm(factory) {
  const button = factory.createButton();
  const input = factory.createInput();
  return input.render() + '\n' + button.render();
}

// 运行时决定用哪个工厂
const platform = 'web';
const factory = platform === 'web' ? new WebUIFactory() : new MobileUIFactory();
console.log(renderForm(factory));

代码示例二:多数据库适配

javascriptCode
// 用抽象工厂封装不同数据库的连接与查询构造器
class MySQLConnection {
  connect() { return 'MySQL 已连接'; }
}
class MySQLQueryBuilder {
  select(table) { return `SELECT * FROM ${table} LIMIT 100`; }
}

class PostgresConnection {
  connect() { return 'Postgres 已连接'; }
}
class PostgresQueryBuilder {
  select(table) { return `SELECT * FROM "${table}" FETCH FIRST 100 ROWS ONLY`; }
}

class DatabaseFactory {
  createConnection() {}
  createQueryBuilder() {}
}

class MySQLFactory extends DatabaseFactory {
  createConnection() { return new MySQLConnection(); }
  createQueryBuilder() { return new MySQLQueryBuilder(); }
}

class PostgresFactory extends DatabaseFactory {
  createConnection() { return new PostgresConnection(); }
  createQueryBuilder() { return new PostgresQueryBuilder(); }
}

function bootstrap(factory) {
  const conn = factory.createConnection();
  const qb = factory.createQueryBuilder();
  console.log(conn.connect());
  console.log(qb.select('users'));
}

bootstrap(new PostgresFactory());

真实案例

Ant Design、Element Plus 等组件库的主题系统底层思想接近抽象工厂:一个 `theme` 配置对象决定了整套组件的视觉产品族。而 React Native 的 `Platform.select` API,本质上也是在为不同平台选择不同的产品实现。

与相近模式对比

| 维度 | 工厂方法 | 抽象工厂 |

| --- | --- | --- |

| 创建对象数量 | 一种产品 | 一族相关产品 |

| 关注点 | 单个对象的创建延迟 | 产品之间的一致性 |

| 扩展新产品 | 容易(加一个工厂方法子类) | 困难(要改抽象工厂接口) |

| 扩展新产品族 | 不适用 | 容易(加一个具体工厂) |

常见坑

产品族扩展 vs 产品等级扩展的矛盾:抽象工厂对"增加新产品族"友好,但对"给每个产品族增加新产品种类"极不友好——每加一种产品就要改所有工厂。设计前必须想清楚哪个维度更容易变化。
过度设计:如果你的产品之间并没有"必须搭配使用"的强约束,用抽象工厂就是杀鸡用牛刀,普通工厂函数足矣。
JS 中不必强行 extends:JavaScript 是动态语言,工厂可以直接返回带对应方法的普通对象,不必像 Java 那样层层继承。

最佳实践

只在存在明确"产品族"概念、且产品间有一致性约束时使用。
工厂选择逻辑集中在应用启动阶段(bootstrap),避免散落在业务代码里。
结合配置对象和 JS 对象字面量,可以用极简方式实现抽象工厂效果。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 创建一族相互关联对象的接口 |

| 最适用场景 | 跨平台 UI、多主题、多数据库适配 |

| 核心收益 | 保证产品族一致性、切换成本低 |

| 主要代价 | 增加新产品种类困难 |

创建型模式深入:建造者 Builder

概念与类比

建造者模式(Builder)把一个复杂对象的构建过程拆解成一系列步骤,让你可以"按部就班"地组装它。

类比点外卖时的"定制汉堡":你先选面包,再选肉饼,再加芝士、生菜、酱料,最后打包。每一步都是可选的、可组合的,最终得到一个满足你个性化需求的汉堡。制作流程(Builder)和最终产品(汉堡)是分离的,同样的流程可以做出千变万化的汉堡。

为什么重要

当一个对象的构造函数参数超过 4 个,尤其是有很多可选参数时,代码会变得极难阅读和维护。这就是臭名昭著的"望远镜构造函数(telescoping constructor)"反模式:

javascriptCode
// 反模式:参数太多,调用时根本看不懂每个值是什么意思
new HttpRequest('https://api.com', 'POST', headers, body, 5000, 3, true, false, null);

建造者模式用链式调用 + 语义化方法名彻底解决这个问题,让构建过程自文档化。

原理与结构

核心是让每个设置方法返回 `this`,从而支持链式调用,最后用一个 `build()` 方法产出最终对象。

代码示例一:HTTP 请求建造者

javascriptCode
class HttpRequestBuilder {
  constructor(url) {
    this.config = {
      url,
      method: 'GET',
      headers: {},
      body: null,
      timeout: 3000,
      retries: 0,
    };
  }

  method(m) {
    this.config.method = m;
    return this;
  }

  header(key, value) {
    this.config.headers[key] = value;
    return this;
  }

  body(data) {
    this.config.body = JSON.stringify(data);
    this.header('Content-Type', 'application/json');
    return this;
  }

  timeout(ms) {
    this.config.timeout = ms;
    return this;
  }

  retries(n) {
    this.config.retries = n;
    return this;
  }

  build() {
    // 构建前可做校验
    if (!this.config.url) {
      throw new Error('URL 不能为空');
    }
    return { ...this.config };
  }
}

// 使用:每一步意图都一目了然
const request = new HttpRequestBuilder('https://api.example.com/users')
  .method('POST')
  .header('Authorization', 'Bearer token123')
  .body({ name: 'Alice', age: 30 })
  .timeout(5000)
  .retries(3)
  .build();

console.log(request);

代码示例二:SQL 查询建造者

javascriptCode
class QueryBuilder {
  constructor() {
    this.parts = { table: '', columns: ['*'], wheres: [], orders: [], limitN: null };
  }

  from(table) {
    this.parts.table = table;
    return this;
  }

  select(...columns) {
    this.parts.columns = columns.length ? columns : ['*'];
    return this;
  }

  where(condition) {
    this.parts.wheres.push(condition);
    return this;
  }

  orderBy(column, direction = 'ASC') {
    this.parts.orders.push(`${column} ${direction}`);
    return this;
  }

  limit(n) {
    this.parts.limitN = n;
    return this;
  }

  build() {
    let sql = `SELECT ${this.parts.columns.join(', ')} FROM ${this.parts.table}`;
    if (this.parts.wheres.length) {
      sql += ' WHERE ' + this.parts.wheres.join(' AND ');
    }
    if (this.parts.orders.length) {
      sql += ' ORDER BY ' + this.parts.orders.join(', ');
    }
    if (this.parts.limitN != null) {
      sql += ` LIMIT ${this.parts.limitN}`;
    }
    return sql;
  }
}

const sql = new QueryBuilder()
  .select('id', 'name', 'email')
  .from('users')
  .where('age > 18')
  .where("status = 'active'")
  .orderBy('created_at', 'DESC')
  .limit(20)
  .build();

console.log(sql);
// SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY created_at DESC LIMIT 20

真实案例

Knex.js / TypeORM 的查询构造器:几乎是建造者模式的教科书实现。
Webpack / Vite 配置:虽然是对象字面量,但很多工具提供链式 config API(如 `webpack-chain`)也是建造者。
测试数据构造:测试中常用 Builder 快速构造带默认值的测试对象(Object Mother / Test Data Builder 模式)。

与相近模式对比

| 维度 | 建造者 | 工厂方法 | 抽象工厂 |

| --- | --- | --- | --- |

| 构建复杂度 | 分多步构建复杂对象 | 一步创建 | 一步创建一族 |

| 是否关注过程 | 关注构建过程 | 不关注 | 不关注 |

| 典型标志 | 链式调用 + build() | create() | createXxx() 多个 |

常见坑

忘记返回 this:链式调用的每个方法必须 `return this`,否则链条断裂。
build() 返回引用而非副本:若直接返回内部 config,外部修改会污染 builder,建议返回浅拷贝或深拷贝。
可变状态复用:同一个 builder 实例 build 多次会共享状态,需谨慎。

最佳实践

参数超过 4 个或有大量可选参数时优先考虑建造者。
在 `build()` 中集中做参数校验,保证产出对象始终合法。
结合 TypeScript 时可用类型让链式调用具备强类型提示。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 分步构建复杂对象,分离构建与表示 |

| 最适用场景 | 多可选参数对象、查询构造、配置对象 |

| 核心收益 | 消除望远镜构造函数、代码自文档化 |

| 主要代价 | 需要额外的 Builder 类 |

创建型模式深入:原型 Prototype

概念与类比

原型模式(Prototype)通过"克隆"一个已有对象来创建新对象,而不是通过 new 从零构造。

类比细胞分裂:与其从原子开始重新合成一个细胞,不如让现有细胞复制自己。当创建一个对象的成本很高(比如需要复杂计算、数据库查询、网络请求)时,克隆一个已有实例比重新创建快得多。

为什么重要

JavaScript 本身就是基于原型的语言,`Object.create`、原型链、`__proto__` 都是原型思想的体现。理解原型模式,等于理解 JS 语言的底层对象模型。此外,在需要大量相似对象、且初始化成本高的场景(如游戏中的子弹、粒子系统),原型模式能显著提升性能。

代码示例一:基于 Object.create 的原型

javascriptCode
const carPrototype = {
  init(brand, color) {
    this.brand = brand;
    this.color = color;
    return this;
  },
  drive() {
    return `${this.color} 的 ${this.brand} 正在行驶`;
  },
  clone() {
    // 以自身为原型创建新对象
    return Object.create(this);
  },
};

const redCar = Object.create(carPrototype).init('Tesla', '红色');
console.log(redCar.drive());

// 克隆并修改
const blueCar = redCar.clone().init('Tesla', '蓝色');
console.log(blueCar.drive());

代码示例二:深浅拷贝的克隆

javascriptCode
class Shape {
  constructor(type, config) {
    this.type = type;
    this.config = config; // 可能是嵌套对象
  }

  // 浅拷贝克隆
  shallowClone() {
    const cloned = new Shape(this.type, this.config);
    return cloned;
  }

  // 深拷贝克隆
  deepClone() {
    return new Shape(
      this.type,
      structuredClone(this.config) // 现代浏览器原生深拷贝 API
    );
  }
}

const original = new Shape('circle', { radius: 10, style: { color: 'red' } });
const shallow = original.shallowClone();
const deep = original.deepClone();

shallow.config.style.color = 'blue';
console.log(original.config.style.color); // blue(被污染)
deep.config.style.color = 'green';
console.log(original.config.style.color); // 仍然是 blue(深拷贝隔离)

深浅拷贝性能对比

| 方式 | 相对速度 | 是否隔离嵌套 | 能否拷贝函数 | 能否处理循环引用 |

| --- | --- | --- | --- | --- |

| 展开运算符 ... | 最快(约 1x) | 否(仅一层) | 是(引用) | 是 |

| Object.assign | 快(约 1.1x) | 否 | 是(引用) | 是 |

| JSON.parse(JSON.stringify) | 慢(约 8-10x) | 是 | 否(丢失) | 否(报错) |

| structuredClone | 中(约 3-4x) | 是 | 否(报错) | 是 |

| 递归深拷贝 | 慢(约 5-8x) | 是 | 可定制 | 需手动处理 |

常见坑

JSON 深拷贝会丢失:`undefined`、函数、`Symbol` 会被丢弃,`Date` 变成字符串,`NaN`/`Infinity` 变成 `null`,循环引用直接报错。
原型链共享:`Object.create` 克隆出的对象共享原型上的属性,修改原型会影响所有克隆体。
性能陷阱:不要在热路径里滥用 `JSON.parse(JSON.stringify)` 做深拷贝,它是最慢的方案之一。

最佳实践

需要深拷贝且无函数时,优先用原生 `structuredClone`。
大量高成本对象创建场景,用原型缓存 + clone 提升性能。
明确区分深浅拷贝的语义,避免克隆体意外污染原对象。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 通过克隆已有对象来创建新对象 |

| 最适用场景 | 高成本对象创建、JS 原型继承 |

| 核心收益 | 避免重复初始化开销 |

| 主要代价 | 深浅拷贝语义易混淆 |

结构型模式深入:适配器 Adapter

概念与类比

适配器模式(Adapter)把一个类的接口转换成客户端期望的另一个接口,让原本因接口不兼容而不能一起工作的类能够协同工作。

最经典的类比就是电源转换插头:你的笔记本是两脚插头,墙上是三孔插座,插不进去。买一个转换插头(适配器),一端接你的插头,一端插墙,问题解决。适配器不改变任何一方,只在中间做转换。

为什么重要

前端开发中,适配器无处不在:对接第三方 SDK(微信/支付宝/Google 登录返回的用户数据字段五花八门)、封装后端返回的数据格式、抹平不同浏览器 API 差异、迁移旧系统时兼容老接口。适配器是"防腐层(Anti-Corruption Layer)"的核心实现手段,能把外部系统的混乱挡在业务代码之外。

代码示例一:统一第三方登录数据

javascriptCode
// 三个平台返回的用户数据格式完全不同
const wechatData = { openid: 'wx123', nickname: '小明', headimgurl: 'a.jpg' };
const googleData = { sub: 'g456', name: 'Ming', picture: 'b.jpg' };
const githubData = { id: 789, login: 'ming', avatar_url: 'c.jpg' };

// 业务系统期望的统一格式
// { id, name, avatar }

// 适配器们
const adapters = {
  wechat: (d) => ({ id: d.openid, name: d.nickname, avatar: d.headimgurl }),
  google: (d) => ({ id: d.sub, name: d.name, avatar: d.picture }),
  github: (d) => ({ id: String(d.id), name: d.login, avatar: d.avatar_url }),
};

function normalizeUser(platform, rawData) {
  const adapt = adapters[platform];
  if (!adapt) throw new Error(`不支持的平台: ${platform}`);
  return adapt(rawData);
}

console.log(normalizeUser('wechat', wechatData));
console.log(normalizeUser('google', googleData));
console.log(normalizeUser('github', githubData));
// 三者输出结构完全一致,业务代码无需关心来源

代码示例二:类适配器包装旧 API

javascriptCode
// 旧的日志库,接口老旧
class LegacyLogger {
  writeLog(level, msg) {
    console.log(`[${level}] ${msg}`);
  }
}

// 新系统期望的接口:logger.info() / logger.error()
class LoggerAdapter {
  constructor() {
    this.legacy = new LegacyLogger();
  }
  info(msg) {
    this.legacy.writeLog('INFO', msg);
  }
  warn(msg) {
    this.legacy.writeLog('WARN', msg);
  }
  error(msg) {
    this.legacy.writeLog('ERROR', msg);
  }
}

const logger = new LoggerAdapter();
logger.info('系统启动');
logger.error('发生错误');

真实案例

axios 的请求/响应拦截器常被用来做数据适配。
Redux 的 `bindActionCreators` 适配了 dispatch 调用方式。
数据库 ORM 把各种数据库的原生驱动适配成统一的查询接口。

与相近模式对比

| 模式 | 意图 | 何时用 |

| --- | --- | --- |

| 适配器 | 转换已有接口 | 事后兼容不兼容的接口 |

| 装饰器 | 增强功能,接口不变 | 动态添加职责 |

| 外观 | 简化复杂子系统 | 提供统一简单入口 |

| 代理 | 控制访问,接口不变 | 加访问控制/懒加载 |

常见坑

适配器过多导致维护困难:如果适配逻辑频繁变化,考虑用配置驱动而非硬编码适配器。
双向适配复杂度:需要双向转换时,代码量翻倍,要评估必要性。

最佳实践

把所有对外部系统的适配集中在防腐层,业务层只用统一模型。
用纯函数实现适配逻辑,便于单元测试。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 转换接口使不兼容的类协同工作 |

| 最适用场景 | 第三方 SDK、遗留系统、数据格式统一 |

| 核心收益 | 隔离外部变化、保护业务代码 |

| 主要代价 | 增加一层间接 |

结构型模式深入:桥接 Bridge

概念与类比

桥接模式(Bridge)将抽象部分与实现部分分离,使它们可以独立变化。

类比遥控器和电视:遥控器是"抽象"(定义了开关、音量、换台等操作),电视是"实现"(具体怎么开机、怎么调音量)。同一个遥控器可以控制不同品牌的电视,同一台电视也可以被不同款式的遥控器控制。两者通过标准化的信号协议(桥)连接,各自独立演进。

为什么重要

桥接解决的是"多维度变化"的组合爆炸问题。假设你有 3 种消息类型(普通、加急、紧急)× 3 种发送渠道(短信、邮件、站内信),如果用继承要写 9 个类;随着维度增加,类的数量呈乘法增长。桥接把两个维度拆开,只需 3 + 3 = 6 个类,用组合替代继承。

代码示例:消息发送系统

javascriptCode
// 实现维度:发送渠道
class SmsSender {
  send(content) {
    return `[短信] ${content}`;
  }
}
class EmailSender {
  send(content) {
    return `[邮件] ${content}`;
  }
}
class InAppSender {
  send(content) {
    return `[站内信] ${content}`;
  }
}

// 抽象维度:消息类型(持有一个实现的引用,这就是"桥")
class Message {
  constructor(sender) {
    this.sender = sender; // 桥接点
  }
  send(text) {
    throw new Error('子类实现');
  }
}

class NormalMessage extends Message {
  send(text) {
    return this.sender.send(text);
  }
}

class UrgentMessage extends Message {
  send(text) {
    return this.sender.send('【加急】' + text);
  }
}

class CriticalMessage extends Message {
  send(text) {
    // 紧急消息可以走多渠道,这里简化
    return this.sender.send('【紧急!!】' + text);
  }
}

// 任意组合两个维度
console.log(new UrgentMessage(new SmsSender()).send('服务器告警'));
console.log(new CriticalMessage(new EmailSender()).send('数据库宕机'));
console.log(new NormalMessage(new InAppSender()).send('欢迎登录'));

类数量对比

| 方案 | 消息类型 3 × 渠道 3 | 消息类型 5 × 渠道 4 |

| --- | --- | --- |

| 纯继承 | 9 个类 | 20 个类 |

| 桥接(组合) | 3 + 3 = 6 个类 | 5 + 4 = 9 个类 |

| 增长趋势 | 乘法 O(m×n) | 加法 O(m+n) |

常见坑

过早桥接:只有一个维度会变化时,桥接是过度设计。
抽象与实现职责划分不清:要明确哪个维度是"做什么"(抽象),哪个是"怎么做"(实现)。

最佳实践

识别出两个及以上独立变化的维度时才使用桥接。
优先"组合优于继承",桥接是这一原则的典型体现。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 分离抽象与实现,让二者独立变化 |

| 最适用场景 | 多维度变化、避免继承爆炸 |

| 核心收益 | 用加法替代乘法的类增长 |

| 主要代价 | 增加理解成本 |

结构型模式深入:组合 Composite

概念与类比

组合模式(Composite)将对象组织成树形结构,让客户端可以用统一的方式处理单个对象和对象组合。

最直观的类比是文件系统:文件夹里可以放文件,也可以放子文件夹,子文件夹里又可以放文件和更深的文件夹。当你计算一个文件夹的总大小时,无论里面是文件还是子文件夹,都用同样的"求大小"操作递归处理。"部分-整体"的层次结构被统一对待。

为什么重要

前端的 DOM 树、组件树(React/Vue)、菜单树、组织架构树、评论嵌套回复——全都是组合模式。理解组合模式,就理解了这些树形 UI 的本质。它让"遍历整棵树""对整棵树执行某操作"变得极其简洁。

代码示例:文件系统

javascriptCode
// 统一接口:都有 getSize 和 print
class FileLeaf {
  constructor(name, size) {
    this.name = name;
    this.size = size;
  }
  getSize() {
    return this.size;
  }
  print(indent = '') {
    console.log(`${indent}📄 ${this.name} (${this.size}KB)`);
  }
}

class Folder {
  constructor(name) {
    this.name = name;
    this.children = [];
  }
  add(child) {
    this.children.push(child);
    return this;
  }
  getSize() {
    // 递归求和:不区分子节点是文件还是文件夹
    return this.children.reduce((sum, child) => sum + child.getSize(), 0);
  }
  print(indent = '') {
    console.log(`${indent}📁 ${this.name}/ (${this.getSize()}KB)`);
    this.children.forEach((child) => child.print(indent + '  '));
  }
}

const root = new Folder('root');
const src = new Folder('src');
src.add(new FileLeaf('index.js', 10)).add(new FileLeaf('utils.js', 5));
root.add(src).add(new FileLeaf('README.md', 3));

root.print();
console.log('总大小:', root.getSize(), 'KB');

代码示例二:菜单树遍历

javascriptCode
const menu = new Folder('主菜单');
const settings = new Folder('设置');
settings.add(new FileLeaf('个人资料', 0)).add(new FileLeaf('隐私', 0));
menu.add(new FileLeaf('首页', 0)).add(settings);

// 用统一方式收集所有叶子节点名称
function collectLeaves(node, acc = []) {
  if (node instanceof FileLeaf) {
    acc.push(node.name);
  } else {
    node.children.forEach((c) => collectLeaves(c, acc));
  }
  return acc;
}
console.log(collectLeaves(menu)); // ['首页', '个人资料', '隐私']

常见坑

叶子节点被迫实现无意义方法:如果统一接口里有 `add`,叶子节点该怎么办?两种设计:透明式(叶子也有 add 但抛异常)vs 安全式(只有容器有 add)。JS 中推荐安全式。
深层递归栈溢出:极深的树用递归遍历可能栈溢出,必要时改用迭代 + 显式栈。

最佳实践

保持叶子与容器的公共接口最小化、语义一致。
树很深时考虑迭代遍历或尾递归优化。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 树形结构中统一处理单体与组合 |

| 最适用场景 | 文件系统、组件树、菜单、组织架构 |

| 核心收益 | 递归操作代码极简、客户端无需区分类型 |

| 主要代价 | 类型约束变弱 |

结构型模式深入:外观 Facade

概念与类比

外观模式(Facade)为复杂的子系统提供一个简化的统一入口。

类比餐厅点餐:你只需对服务员说"来一份宫保鸡丁",服务员(外观)会去协调后厨的采购、切配、炒制、摆盘等一系列复杂流程,你不需要知道后厨内部怎么运作。外观把复杂性藏在背后,对外只暴露简单接口。

为什么重要

外观是降低系统耦合度的利器。当多个模块需要协同完成一件事时,与其让调用方了解所有模块并按正确顺序调用,不如提供一个外观方法一键搞定。前端封装的各种 `utils`、`service`、SDK 初始化函数,本质上都是外观。

代码示例:视频转码外观

javascriptCode
// 复杂的子系统
class VideoDecoder {
  decode(file) {
    console.log('解码视频:', file);
    return 'decoded-data';
  }
}
class AudioProcessor {
  process(data) {
    console.log('处理音频');
    return data + '-audio';
  }
}
class Compressor {
  compress(data, quality) {
    console.log(`压缩,质量 ${quality}`);
    return data + '-compressed';
  }
}
class FileWriter {
  write(data, format) {
    console.log(`写入 ${format} 文件`);
    return `output.${format}`;
  }
}

// 外观:一个方法搞定整个流程
class VideoConverterFacade {
  constructor() {
    this.decoder = new VideoDecoder();
    this.audio = new AudioProcessor();
    this.compressor = new Compressor();
    this.writer = new FileWriter();
  }

  convert(file, { format = 'mp4', quality = 80 } = {}) {
    let data = this.decoder.decode(file);
    data = this.audio.process(data);
    data = this.compressor.compress(data, quality);
    return this.writer.write(data, format);
  }
}

// 调用方只需一行
const converter = new VideoConverterFacade();
const result = converter.convert('input.avi', { format: 'mp4', quality: 90 });
console.log('完成:', result);

与相近模式对比

| 模式 | 意图 | 是否新增功能 |

| --- | --- | --- |

| 外观 | 简化子系统访问 | 否,只是编排 |

| 适配器 | 转换接口 | 否 |

| 中介者 | 协调对象间通信 | 否,但双向 |

常见坑

外观变成上帝对象:外观方法越堆越多,最终变成一个无所不包的巨型类。应按业务领域拆分多个外观。
过度封装:如果子系统本身就简单,加外观反而多此一举。

最佳实践

外观只做编排,不写业务逻辑。
允许高级用户绕过外观直接访问子系统(外观不应封死底层)。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 为复杂子系统提供简化统一入口 |

| 最适用场景 | SDK 封装、多模块协同、简化 API |

| 核心收益 | 降低耦合、易用性提升 |

| 主要代价 | 可能变成上帝对象 |

结构型模式深入:享元 Flyweight

概念与类比

享元模式(Flyweight)通过共享技术高效支持大量细粒度对象,把对象的可共享部分(内部状态)与不可共享部分(外部状态)分离。

类比图书馆:一本《三体》可能有 100 个读者在读,图书馆不会为每个读者印一本,而是共享同几本,"谁在读、读到第几页"(外部状态)由读者自己记录,"书的内容"(内部状态)大家共享。

为什么重要

在需要创建海量相似对象的场景(如地图上的成千上万个标记点、游戏中的大量子弹/树木、编辑器中每个字符),如果每个对象都独立存储所有数据,内存会爆炸。享元通过共享不变部分,能把内存占用降低一个数量级。

代码示例:地图标记点

javascriptCode
// 享元工厂:缓存共享的内部状态(图标类型)
class MarkerIconFactory {
  constructor() {
    this.icons = new Map();
  }
  getIcon(type) {
    if (!this.icons.has(type)) {
      // 假设图标对象很大(包含图片数据等)
      console.log(`创建新图标: ${type}`);
      this.icons.set(type, { type, image: `[${type}图片数据 20KB]` });
    }
    return this.icons.get(type);
  }
  get count() {
    return this.icons.size;
  }
}

const factory = new MarkerIconFactory();

// 外部状态(坐标)由每个标记独立持有,内部状态(图标)共享
class Marker {
  constructor(lng, lat, type) {
    this.lng = lng; // 外部状态
    this.lat = lat; // 外部状态
    this.icon = factory.getIcon(type); // 内部状态,共享
  }
}

// 创建 10000 个标记,但只有 3 种图标
const markers = [];
const types = ['restaurant', 'hotel', 'gas'];
for (let i = 0; i < 10000; i++) {
  markers.push(new Marker(Math.random(), Math.random(), types[i % 3]));
}

console.log('标记数量:', markers.length);
console.log('实际图标对象数:', factory.count); // 只有 3 个

内存对比

| 方案 | 10000 个标记,图标 20KB | 内存占用 |

| --- | --- | --- |

| 不用享元(每个标记独立图标) | 10000 × 20KB | 约 200MB |

| 用享元(3 种图标共享) | 3 × 20KB + 10000 × 坐标 | 约 60KB + 少量 |

| 内存节省 | —— | 约 99.97% |

常见坑

内部/外部状态划分错误:把会变化的状态当成内部状态共享,会导致数据串台。只有真正不变、可共享的才是内部状态。
共享池无限增长:如果 key 种类无限,缓存池本身会变成内存泄漏源,需设上限或用 LRU。

最佳实践

只有对象数量巨大且存在大量重复内部状态时才用享元。
严格区分内部(可共享、不变)与外部(每对象独有、可变)状态。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 共享细粒度对象的不变部分以节省内存 |

| 最适用场景 | 海量相似对象、地图标记、粒子、字符 |

| 核心收益 | 内存占用大幅下降 |

| 主要代价 | 代码复杂度上升、状态划分难 |

行为型模式深入:责任链 Chain of Responsibility

概念与类比

责任链模式(Chain of Responsibility)让多个对象都有机会处理请求,把这些对象连成一条链,请求沿链传递直到有对象处理它为止。

类比公司报销审批:你提交报销单,先到组长,组长权限内的直接批,超权限的转交部门经理,再超转交财务总监,再超转交 CEO。每一级只处理自己权限范围内的请求,处理不了就往上传。请求发起者不需要知道最终是谁批的。

为什么重要

责任链最大的价值是解耦请求发送者与接收者,并让处理逻辑高度可插拔。Express/Koa 的中间件、axios 拦截器、事件冒泡、表单校验链、日志过滤链,本质都是责任链。它让你可以像搭积木一样增删处理环节,符合开闭原则。

代码示例:请求审批链

javascriptCode
class Approver {
  constructor(name, limit) {
    this.name = name;
    this.limit = limit;
    this.next = null;
  }

  setNext(approver) {
    this.next = approver;
    return approver; // 便于链式设置
  }

  handle(amount) {
    if (amount <= this.limit) {
      console.log(`${this.name} 批准了 ${amount} 元的报销`);
    } else if (this.next) {
      console.log(`${this.name} 权限不足,上报`);
      this.next.handle(amount);
    } else {
      console.log(`没有人能批准 ${amount} 元`);
    }
  }
}

const leader = new Approver('组长', 1000);
const manager = new Approver('经理', 5000);
const director = new Approver('总监', 20000);

leader.setNext(manager).setNext(director);

leader.handle(800);   // 组长批
leader.handle(3000);  // 经理批
leader.handle(15000); // 总监批
leader.handle(50000); // 没人能批

代码示例二:函数式中间件洋葱模型

javascriptCode
// Koa 风格的中间件组合(compose)
function compose(middlewares) {
  return function (context) {
    let index = -1;
    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next 被多次调用'));
      index = i;
      const fn = middlewares[i];
      if (!fn) return Promise.resolve();
      return Promise.resolve(fn(context, () => dispatch(i + 1)));
    }
    return dispatch(0);
  };
}

const logger = async (ctx, next) => {
  console.log('-> 进入 logger');
  await next();
  console.log('<- 离开 logger');
};

const auth = async (ctx, next) => {
  console.log('-> 进入 auth');
  ctx.user = 'Alice';
  await next();
  console.log('<- 离开 auth');
};

const handler = async (ctx, next) => {
  console.log('   处理业务, 用户:', ctx.user);
  await next();
};

const app = compose([logger, auth, handler]);
app({}).then(() => console.log('全部完成'));
// 输出体现"洋葱模型":进入顺序 logger->auth->handler,返回逆序

常见坑

链断裂:某个处理者忘记调用 `next`,后续环节永远收不到请求。
无人处理:请求走到链尾没人处理,需要有兜底逻辑。
性能:链过长会增加调用栈深度和延迟。

最佳实践

每个处理者职责单一,只关心自己能否处理。
提供链尾兜底处理者,避免请求"消失"。
用数组 + compose 的函数式写法比对象链更灵活。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 请求沿处理者链传递直到被处理 |

| 最适用场景 | 中间件、审批流、过滤器、事件冒泡 |

| 核心收益 | 发送者与接收者解耦、处理环节可插拔 |

| 主要代价 | 请求可能无人处理、调试链路难 |

行为型模式深入:命令 Command

概念与类比

命令模式(Command)将请求封装成一个独立对象,从而可以用不同的请求参数化其他对象,并支持请求的排队、记录、撤销。

类比餐厅点菜单:你点的菜被写成一张订单(命令对象),服务员把订单交给后厨。订单把"谁点的、点了什么"封装起来,可以排队、可以取消、可以重做。点菜人和做菜人通过订单解耦,互不直接依赖。

为什么重要

命令模式是实现撤销/重做(undo/redo)操作日志宏命令(批量操作)任务队列的基石。编辑器、绘图软件、事务系统都离不开它。Redux 的 action 本质就是命令对象。

代码示例:可撤销的文本编辑器

javascriptCode
// 接收者
class Editor {
  constructor() {
    this.content = '';
  }
}

// 命令基类
class Command {
  execute() {}
  undo() {}
}

// 具体命令:插入文本
class InsertCommand extends Command {
  constructor(editor, text) {
    super();
    this.editor = editor;
    this.text = text;
  }
  execute() {
    this.editor.content += this.text;
  }
  undo() {
    this.editor.content = this.editor.content.slice(0, -this.text.length);
  }
}

// 命令管理器:支持撤销/重做
class CommandManager {
  constructor() {
    this.history = [];
    this.redoStack = [];
  }
  run(command) {
    command.execute();
    this.history.push(command);
    this.redoStack = []; // 新操作清空重做栈
  }
  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.redoStack.push(command);
    }
  }
  redo() {
    const command = this.redoStack.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }
}

const editor = new Editor();
const manager = new CommandManager();

manager.run(new InsertCommand(editor, 'Hello '));
manager.run(new InsertCommand(editor, 'World'));
console.log(editor.content); // Hello World

manager.undo();
console.log(editor.content); // Hello

manager.redo();
console.log(editor.content); // Hello World

代码示例二:宏命令(批量执行)

javascriptCode
class MacroCommand {
  constructor() {
    this.commands = [];
  }
  add(command) {
    this.commands.push(command);
    return this;
  }
  execute() {
    this.commands.forEach((c) => c.execute());
  }
  undo() {
    // 逆序撤销
    [...this.commands].reverse().forEach((c) => c.undo());
  }
}

const editor2 = new Editor();
const macro = new MacroCommand()
  .add(new InsertCommand(editor2, 'A'))
  .add(new InsertCommand(editor2, 'B'))
  .add(new InsertCommand(editor2, 'C'));

macro.execute();
console.log(editor2.content); // ABC
macro.undo();
console.log(editor2.content); // 空

常见坑

命令对象膨胀:每个操作一个类,简单场景下类爆炸。可用函数 + 闭包简化。
撤销状态管理复杂:命令需要保存足够的信息才能正确撤销,状态多时易出错。可考虑备忘录模式配合。

最佳实践

撤销/重做、任务队列、日志场景优先考虑命令模式。
命令对象应自包含(携带执行和撤销所需的全部信息)。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 把请求封装成对象以支持队列、日志、撤销 |

| 最适用场景 | 编辑器 undo/redo、任务队列、事务 |

| 核心收益 | 请求可存储、可撤销、可组合 |

| 主要代价 | 命令类数量多 |

行为型模式深入:迭代器 Iterator

概念与类比

迭代器模式(Iterator)提供一种方法顺序访问聚合对象中的元素,而不暴露其内部表示。

类比自动售货机取货:你按下按钮,机器一个接一个吐出商品,你不需要知道货物在机器内部是怎么排列存放的。迭代器就是这个"吐货口",提供统一的"下一个"操作。

为什么重要

JavaScript 把迭代器提升为语言级协议(Iteration Protocol),`for...of`、展开运算符、解构、`Array.from` 都基于它。理解迭代器协议,才能自定义可迭代对象、玩转生成器、处理无限序列和惰性求值。

代码示例:自定义可迭代对象

javascriptCode
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  // 实现迭代器协议
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    const step = this.step;
    return {
      next() {
        if (current < end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  }
}

const range = new Range(0, 10, 2);
for (const num of range) {
  console.log(num); // 0 2 4 6 8
}
console.log([...range]); // [0, 2, 4, 6, 8]

代码示例二:生成器实现惰性无限序列

javascriptCode
// 生成器是实现迭代器最简洁的方式
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// 惰性求值:只取前 10 个,不会无限循环
function take(iterator, n) {
  const result = [];
  for (const value of iterator) {
    if (result.length >= n) break;
    result.push(value);
  }
  return result;
}

console.log(take(fibonacci(), 10)); // [0,1,1,2,3,5,8,13,21,34]

常见坑

迭代器状态不可重置:普通迭代器一次性消费,重新遍历需要重新创建。
修改集合导致迭代异常:遍历过程中增删元素可能引发不可预期行为。

最佳实践

优先用生成器函数实现迭代器,代码最简洁。
处理大数据/无限流时用惰性迭代避免一次性加载。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 统一顺序访问集合元素而不暴露内部结构 |

| 最适用场景 | 自定义集合、惰性序列、无限流 |

| 核心收益 | 遍历方式统一、支持惰性求值 |

| 主要代价 | 一次性消费、需理解协议 |

行为型模式深入:中介者 Mediator

概念与类比

中介者模式(Mediator)用一个中介对象来封装一系列对象之间的交互,使各对象不需要显式相互引用,从而降低耦合。

类比机场塔台:飞机之间不直接通信协商起降顺序(那样会乱成一团),而是全部听塔台指挥。塔台(中介者)掌握全局,协调所有飞机。每架飞机只和塔台通信,彼此解耦。

为什么重要

当多个对象两两之间需要交互时,直接互相引用会形成 O(n²) 的网状耦合,牵一发动全身。中介者把网状结构改成星型结构,所有交互经过中介,耦合降为 O(n)。表单组件联动、聊天室、复杂 UI 组件通信是典型场景。

代码示例:聊天室

javascriptCode
// 中介者
class ChatRoom {
  constructor() {
    this.users = new Map();
  }
  register(user) {
    this.users.set(user.name, user);
    user.chatroom = this;
    return this;
  }
  send(from, to, message) {
    if (to) {
      // 私聊
      const target = this.users.get(to);
      if (target) target.receive(from, message);
    } else {
      // 广播
      this.users.forEach((user) => {
        if (user.name !== from) user.receive(from, message);
      });
    }
  }
}

class User {
  constructor(name) {
    this.name = name;
    this.chatroom = null;
  }
  send(message, to = null) {
    this.chatroom.send(this.name, to, message);
  }
  receive(from, message) {
    console.log(`[${this.name} 收到] ${from}: ${message}`);
  }
}

const room = new ChatRoom();
const alice = new User('Alice');
const bob = new User('Bob');
const carol = new User('Carol');
room.register(alice).register(bob).register(carol);

alice.send('大家好'); // 广播给 Bob 和 Carol
bob.send('你好 Alice', 'Alice'); // 私聊

与观察者对比

| 维度 | 中介者 | 观察者 |

| --- | --- | --- |

| 通信方向 | 双向、多对多经中心 | 单向、一对多 |

| 中心角色 | 中介者掌握协调逻辑 | 主题只负责通知 |

| 耦合形态 | 星型 | 发布订阅 |

| 典型场景 | 组件联动、聊天室 | 事件系统、状态订阅 |

常见坑

中介者膨胀成上帝对象:所有交互逻辑都塞进中介者,它会变得巨大难维护。需按业务拆分。

最佳实践

只在对象间存在复杂多对多交互时引入中介者。
警惕中介者过度集权,必要时拆分多个中介者。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 用中介对象封装对象间的复杂交互 |

| 最适用场景 | 组件联动、聊天室、复杂表单 |

| 核心收益 | 网状耦合变星型、降低耦合度 |

| 主要代价 | 中介者可能变上帝对象 |

行为型模式深入:备忘录 Memento

概念与类比

备忘录模式(Memento)在不破坏封装的前提下捕获并保存对象的内部状态,以便之后恢复到这个状态。

类比游戏存档:你在打 Boss 前存个档,如果失败了就读档回到存档点。存档文件(备忘录)保存了角色的血量、位置、装备等状态,但你看不到也不需要知道存档文件的内部格式。

为什么重要

备忘录是实现快照、撤销、事务回滚、草稿恢复的核心。它和命令模式常常配合使用:命令模式负责"做什么",备忘录负责"状态怎么回退"。表单草稿自动保存、文档历史版本、游戏存档都是它的应用。

代码示例:编辑器状态快照

javascriptCode
// 备忘录:只存状态,不含逻辑
class EditorMemento {
  constructor(content, cursorPos) {
    this._content = content;
    this._cursorPos = cursorPos;
    Object.freeze(this); // 备忘录应不可变
  }
  getContent() { return this._content; }
  getCursor() { return this._cursorPos; }
}

// 原发器:创建和恢复备忘录
class TextEditor {
  constructor() {
    this.content = '';
    this.cursorPos = 0;
  }
  type(text) {
    this.content += text;
    this.cursorPos = this.content.length;
  }
  save() {
    return new EditorMemento(this.content, this.cursorPos);
  }
  restore(memento) {
    this.content = memento.getContent();
    this.cursorPos = memento.getCursor();
  }
}

// 管理者:保存备忘录历史
class History {
  constructor() {
    this.snapshots = [];
  }
  push(memento) {
    this.snapshots.push(memento);
  }
  pop() {
    return this.snapshots.pop();
  }
}

const editor = new TextEditor();
const history = new History();

editor.type('第一句。');
history.push(editor.save());

editor.type('第二句。');
history.push(editor.save());

editor.type('第三句。');
console.log(editor.content); // 第一句。第二句。第三句。

history.pop(); // 丢弃当前快照
editor.restore(history.pop()); // 恢复到第一句
console.log(editor.content); // 第一句。

与命令模式配合的内存对比

| 撤销实现方式 | 内存占用 | 实现复杂度 | 适用 |

| --- | --- | --- | --- |

| 命令模式(存操作) | 低(只存增量) | 中 | 操作可逆、状态小 |

| 备忘录(存快照) | 高(存全量状态) | 低 | 状态难逆推、快照小 |

| 混合(命令+备忘录) | 中 | 高 | 复杂编辑器 |

常见坑

快照内存爆炸:频繁全量快照会吃光内存,需限制历史长度或存增量。
深浅拷贝问题:备忘录若只存引用,原对象变化会污染快照,必须存副本。

最佳实践

备忘录设为不可变对象(`Object.freeze`)。
限制历史栈长度,或结合命令模式存增量而非全量。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 保存并恢复对象状态而不破坏封装 |

| 最适用场景 | 撤销、快照、草稿、游戏存档 |

| 核心收益 | 状态可回退、封装不破坏 |

| 主要代价 | 快照占内存 |

行为型模式深入:状态 State

概念与类比

状态模式(State)允许对象在其内部状态改变时改变它的行为,看起来就像修改了它的类。

类比自动门:门有"关闭""打开""正在开""正在关"几种状态。同样是"有人靠近"这个事件,门在不同状态下反应完全不同——关闭时开始打开,正在关时反转。每种状态知道自己该怎么响应事件,以及下一步转到哪个状态。

为什么重要

状态模式用来消灭那种臭名昭著的巨型 `if-else`/`switch` 状态判断。当一个对象的行为强依赖于它的状态、且状态间有明确转换规则时(订单流转、播放器、审批流、红绿灯),状态模式把每个状态的逻辑内聚到独立的状态类里,新增状态不影响其他状态。

代码示例:订单状态机

javascriptCode
// 每个状态是一个类,封装该状态下的行为和转换规则
class OrderState {
  constructor(order) {
    this.order = order;
  }
  pay() { console.log('当前状态无法支付'); }
  ship() { console.log('当前状态无法发货'); }
  cancel() { console.log('当前状态无法取消'); }
}

class PendingState extends OrderState {
  pay() {
    console.log('支付成功');
    this.order.setState(this.order.paidState);
  }
  cancel() {
    console.log('订单已取消');
    this.order.setState(this.order.cancelledState);
  }
}

class PaidState extends OrderState {
  ship() {
    console.log('商品已发货');
    this.order.setState(this.order.shippedState);
  }
  cancel() {
    console.log('已支付,走退款取消流程');
    this.order.setState(this.order.cancelledState);
  }
}

class ShippedState extends OrderState {
  // 已发货不能取消也不能重复发货,全部继承默认拒绝行为
}

class CancelledState extends OrderState {
  // 终态,什么都不能做
}

class Order {
  constructor() {
    this.pendingState = new PendingState(this);
    this.paidState = new PaidState(this);
    this.shippedState = new ShippedState(this);
    this.cancelledState = new CancelledState(this);
    this.state = this.pendingState; // 初始状态
  }
  setState(state) {
    this.state = state;
  }
  pay() { this.state.pay(); }
  ship() { this.state.ship(); }
  cancel() { this.state.cancel(); }
}

const order = new Order();
order.ship();   // 当前状态无法发货(待支付不能发货)
order.pay();    // 支付成功 -> 已支付
order.ship();   // 商品已发货 -> 已发货
order.cancel(); // 当前状态无法取消(已发货不能取消)

if-else 与状态模式对比

| 维度 | 巨型 if-else | 状态模式 |

| --- | --- | --- |

| 新增状态 | 修改所有判断分支 | 加一个状态类 |

| 状态逻辑内聚 | 分散在各处 | 集中在状态类 |

| 可读性 | 差(分支嵌套) | 好 |

| 代码量 | 少(初期) | 多(类多) |

| 违反开闭原则 | 是 | 否 |

常见坑

状态类爆炸:状态很多时类数量大,简单状态机用对象映射表更轻量。
状态转换逻辑分散:转换规则散落在各状态类,全局视图不清晰,可配一张状态转换表文档。

最佳实践

状态多且行为差异大时用状态类;状态简单时用对象映射表或状态机库(如 XState)。
维护一张清晰的状态转换图/表。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 状态改变时对象行为随之改变 |

| 最适用场景 | 订单、播放器、审批流、红绿灯 |

| 核心收益 | 消灭巨型 if-else、状态逻辑内聚 |

| 主要代价 | 状态类可能爆炸 |

行为型模式深入:模板方法 Template Method

概念与类比

模板方法模式(Template Method)在父类中定义一个算法的骨架,把某些步骤延迟到子类实现,让子类在不改变算法结构的前提下重定义特定步骤。

类比冲泡饮料:泡咖啡和泡茶的流程骨架一样——烧水、冲泡、倒进杯子、加调料。但"冲泡"和"加调料"这两步不同(咖啡用咖啡粉加糖,茶用茶叶加柠檬)。父类定好流程骨架,子类只填不同的步骤。

为什么重要

模板方法是复用算法骨架、消除重复代码的经典手段。当你有多个流程"大同小异"时,把相同的部分提到父类,把不同的部分做成抽象步骤,子类只实现差异部分。React 组件生命周期、各种框架的钩子(hook)方法都是模板方法思想。

代码示例:数据导出流程

javascriptCode
class DataExporter {
  // 模板方法:定义算法骨架,final 不应被重写
  export(data) {
    const validated = this.validate(data);
    const formatted = this.format(validated);
    const result = this.write(formatted);
    this.afterExport(); // 钩子方法,可选重写
    return result;
  }

  validate(data) {
    if (!Array.isArray(data)) throw new Error('数据必须是数组');
    return data;
  }

  // 抽象步骤:子类必须实现
  format(data) {
    throw new Error('子类必须实现 format');
  }
  write(formatted) {
    throw new Error('子类必须实现 write');
  }

  // 钩子方法:默认空实现,子类可选覆盖
  afterExport() {}
}

class CsvExporter extends DataExporter {
  format(data) {
    return data.map((row) => Object.values(row).join(',')).join('\n');
  }
  write(formatted) {
    return 'CSV:\n' + formatted;
  }
  afterExport() {
    console.log('CSV 导出完成');
  }
}

class JsonExporter extends DataExporter {
  format(data) {
    return JSON.stringify(data, null, 2);
  }
  write(formatted) {
    return 'JSON:\n' + formatted;
  }
}

const data = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }];
console.log(new CsvExporter().export(data));
console.log(new JsonExporter().export(data));

与策略模式对比

| 维度 | 模板方法 | 策略模式 |

| --- | --- | --- |

| 复用方式 | 继承 | 组合 |

| 变化部分 | 算法的某些步骤 | 整个算法 |

| 骨架控制 | 父类固定骨架 | 无固定骨架 |

| 灵活性 | 编译期确定 | 运行期切换 |

常见坑

继承层级过深:模板方法依赖继承,层级一深就脆弱。JS 中可用高阶函数替代。
钩子太多:钩子方法泛滥会让流程难以追踪。

最佳实践

流程骨架稳定、步骤有差异时用模板方法。
JS 中可用高阶函数(把可变步骤作为参数传入)替代继承式模板方法。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 父类定义算法骨架,子类填充步骤 |

| 最适用场景 | 流程相似、步骤有别的算法族 |

| 核心收益 | 复用骨架、消除重复 |

| 主要代价 | 依赖继承、层级易深 |

行为型模式深入:访问者 Visitor

概念与类比

访问者模式(Visitor)将作用于某对象结构中各元素的操作分离出来,封装成独立的访问者对象,从而可以在不修改元素类的情况下定义作用于这些元素的新操作。

类比大楼里的检查员:同一栋楼(对象结构),今天来消防检查员,明天来卫生检查员,后天来安全检查员。每个检查员(访问者)对每个房间(元素)做不同的检查,但房间本身不需要改变。要新增一种检查,只需派一个新检查员,不用改造大楼。

为什么重要

访问者的杀手级场景是编译器/AST 处理。Babel、ESLint、TypeScript 编译器遍历抽象语法树时,对不同类型的节点(函数声明、变量声明、表达式)做不同处理,且经常要新增处理逻辑(新的 lint 规则、新的转换)——访问者让你把每种处理封装成一个访问者,无需改动 AST 节点类。

代码示例:AST 节点处理

javascriptCode
// 元素:AST 节点,都提供 accept 方法
class NumberNode {
  constructor(value) { this.value = value; }
  accept(visitor) { return visitor.visitNumber(this); }
}
class AddNode {
  constructor(left, right) { this.left = left; this.right = right; }
  accept(visitor) { return visitor.visitAdd(this); }
}
class MultiplyNode {
  constructor(left, right) { this.left = left; this.right = right; }
  accept(visitor) { return visitor.visitMultiply(this); }
}

// 访问者一:求值
class EvaluateVisitor {
  visitNumber(node) { return node.value; }
  visitAdd(node) { return node.left.accept(this) + node.right.accept(this); }
  visitMultiply(node) { return node.left.accept(this) * node.right.accept(this); }
}

// 访问者二:打印为中缀表达式(新增操作无需改节点类)
class PrintVisitor {
  visitNumber(node) { return String(node.value); }
  visitAdd(node) { return `(${node.left.accept(this)} + ${node.right.accept(this)})`; }
  visitMultiply(node) { return `(${node.left.accept(this)} * ${node.right.accept(this)})`; }
}

// 构造表达式 (3 + 4) * 5
const ast = new MultiplyNode(new AddNode(new NumberNode(3), new NumberNode(4)), new NumberNode(5));

console.log(ast.accept(new EvaluateVisitor())); // 35
console.log(ast.accept(new PrintVisitor()));    // ((3 + 4) * 5)

常见坑

新增元素类型困难:访问者对"新增操作"友好,但对"新增元素类型"极不友好——每加一种节点,所有访问者都要改。这与抽象工厂的两难异曲同工。
破坏封装:访问者常需要访问元素的内部状态。

最佳实践

元素类型稳定、但操作频繁新增时用访问者(如 AST)。
元素类型频繁变化时不要用访问者。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 把作用于对象结构的操作外置为访问者 |

| 最适用场景 | AST 处理、编译器、报表生成 |

| 核心收益 | 新增操作不改元素类 |

| 主要代价 | 新增元素类型要改所有访问者 |

行为型模式深入:解释器 Interpreter

概念与类比

解释器模式(Interpreter)给定一个语言,定义它的文法表示,并定义一个解释器来解释语言中的句子。

类比翻译官:给定一套语法规则(比如简单的计算表达式),解释器逐条读取、按规则理解并执行。正则引擎、模板引擎、SQL 解析、公式计算器都是解释器的应用。

为什么重要

当你需要处理一种"小语言"(DSL,领域特定语言)——比如自定义的搜索语法、规则引擎表达式、模板占位符——解释器模式提供了系统化的实现思路。它相对小众,但在规则引擎、低代码平台中很关键。

代码示例:布尔表达式解释器

javascriptCode
// 上下文:变量的值
// 终结符表达式
class Variable {
  constructor(name) { this.name = name; }
  interpret(context) { return Boolean(context[this.name]); }
}
class Constant {
  constructor(value) { this.value = value; }
  interpret() { return this.value; }
}

// 非终结符表达式
class And {
  constructor(left, right) { this.left = left; this.right = right; }
  interpret(context) { return this.left.interpret(context) && this.right.interpret(context); }
}
class Or {
  constructor(left, right) { this.left = left; this.right = right; }
  interpret(context) { return this.left.interpret(context) || this.right.interpret(context); }
}
class Not {
  constructor(expr) { this.expr = expr; }
  interpret(context) { return !this.expr.interpret(context); }
}

// 解释 (isVip AND isActive) OR isAdmin
const expression = new Or(
  new And(new Variable('isVip'), new Variable('isActive')),
  new Variable('isAdmin')
);

console.log(expression.interpret({ isVip: true, isActive: true, isAdmin: false }));  // true
console.log(expression.interpret({ isVip: true, isActive: false, isAdmin: false })); // false
console.log(expression.interpret({ isVip: false, isActive: false, isAdmin: true })); // true

常见坑

文法复杂时爆炸:文法一复杂,类的数量和维护成本急剧上升。复杂语言应上专业解析器生成器(如 PEG.js、ANTLR)。
性能:解释执行通常比编译执行慢。

最佳实践

只用于简单、稳定的小语言(DSL)。
文法复杂时用解析器生成工具而非手写解释器。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 为小语言定义文法并解释执行 |

| 最适用场景 | 规则引擎、简单 DSL、公式计算 |

| 核心收益 | 文法可扩展、结构清晰 |

| 主要代价 | 复杂文法难维护、性能一般 |

JavaScript 特色模式:模块模式与揭示模块模式

概念与类比

模块模式(Module Pattern)利用 JavaScript 的闭包,创建私有作用域,只暴露必要的公共接口,实现真正的封装(在 ES Module 出现之前,这是 JS 实现私有变量的主要手段)。

类比公司的前台:外人(外部代码)只能通过前台(公共接口)办事,公司内部的办公室、财务室(私有变量)外人进不去。

代码示例:经典模块模式

javascriptCode
// 立即执行函数(IIFE)创建私有作用域
const counterModule = (function () {
  // 私有变量,外部无法直接访问
  let count = 0;
  let step = 1;

  // 私有函数
  function log(action) {
    console.log(`[counter] ${action}, 当前值: ${count}`);
  }

  // 只暴露公共接口
  return {
    increment() {
      count += step;
      log('increment');
      return count;
    },
    decrement() {
      count -= step;
      log('decrement');
      return count;
    },
    reset() {
      count = 0;
      log('reset');
    },
    getValue() {
      return count;
    },
  };
})();

counterModule.increment(); // 1
counterModule.increment(); // 2
console.log(counterModule.count); // undefined(私有,访问不到)
console.log(counterModule.getValue()); // 2

揭示模块模式

揭示模块模式(Revealing Module Pattern)是模块模式的变体:所有函数和变量都定义在私有作用域中,最后在返回对象里"揭示"哪些是公开的。它的好处是公开映射一目了然,且公共方法之间调用不用加前缀。

javascriptCode
const userService = (function () {
  let users = [];

  function add(user) {
    users.push(user);
    logChange(); // 直接调用私有函数
  }

  function remove(id) {
    users = users.filter((u) => u.id !== id);
    logChange();
  }

  function getAll() {
    return [...users];
  }

  function logChange() {
    console.log(`用户数量: ${users.length}`);
  }

  // 在末尾清晰地"揭示"公共接口
  return {
    add,
    remove,
    getAll,
    // logChange 不暴露,保持私有
  };
})();

userService.add({ id: 1, name: 'Alice' });
userService.add({ id: 2, name: 'Bob' });
console.log(userService.getAll());

模块方案演进对比

| 方案 | 私有性 | 时代 | 现状 |

| --- | --- | --- | --- |

| IIFE 模块模式 | 闭包实现 | ES5 时代 | 已被 ESM 取代 |

| 揭示模块模式 | 闭包实现 | ES5 时代 | 思想仍有价值 |

| CommonJS | 文件级 | Node.js | 后端仍广泛 |

| ES Module | 文件级 + `#` 私有字段 | ES2015+ | 现代标准 |

最佳实践

现代项目直接用 ES Module + class `#` 私有字段。
理解模块模式有助于读懂大量老代码和库源码。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 用闭包实现私有作用域和封装 |

| 最适用场景 | 老代码、库封装、理解 JS 闭包 |

| 核心收益 | 真正的私有变量 |

| 主要代价 | ESM 时代已非首选 |

JavaScript 特色模式:混入 Mixin

概念与类比

混入(Mixin)把一组方法"掺入"到一个类或对象中,实现横向的功能复用,绕开单继承的限制。

类比给手机装 App:手机(类)本身有基础功能,你想要拍照、导航、支付功能,就装上对应的 App(mixin)。功能是"拼装"上去的,一个手机可以装任意多个 App。

为什么重要

JavaScript 只支持单继承,但现实中一个类可能需要多种不相关的能力(可序列化、可比较、可观察)。混入用组合的方式给类"叠加"能力,比深继承链灵活得多。Vue 2 的 mixin、很多工具库的能力增强都用此模式。

代码示例:对象混入

javascriptCode
// 定义可复用的能力片段
const serializable = {
  serialize() {
    return JSON.stringify(this);
  },
};

const comparable = {
  compareTo(other) {
    return this.value - other.value;
  },
};

const observable = {
  observers: [],
  subscribe(fn) {
    this.observers.push(fn);
  },
  notify(data) {
    this.observers.forEach((fn) => fn(data));
  },
};

// 把多个能力混入一个对象
function mixin(target, ...sources) {
  return Object.assign(target, ...sources);
}

const product = mixin(
  { value: 100, name: '商品' },
  serializable,
  comparable
);

console.log(product.serialize()); // 拥有序列化能力
console.log(product.compareTo({ value: 80 })); // 20,拥有比较能力

代码示例二:类混入(高阶函数式)

javascriptCode
// 用返回类的函数实现类混入
const Timestamped = (Base) =>
  class extends Base {
    constructor(...args) {
      super(...args);
      this.createdAt = Date.now();
    }
    getAge() {
      return Date.now() - this.createdAt;
    }
  };

const Activatable = (Base) =>
  class extends Base {
    activate() { this.active = true; }
    deactivate() { this.active = false; }
  };

class Widget {
  constructor(name) {
    this.name = name;
  }
}

// 叠加多个混入
class EnhancedWidget extends Activatable(Timestamped(Widget)) {}

const w = new EnhancedWidget('按钮');
w.activate();
console.log(w.active); // true
console.log(w.name); // 按钮
console.log(typeof w.getAge()); // number

常见坑

命名冲突:多个混入有同名方法会互相覆盖,后混入的赢。
来源不清:一个方法到底来自哪个混入难以追踪,调试困难。
this 依赖:混入方法依赖宿主的属性时耦合隐蔽。

最佳实践

混入方法尽量无状态或状态自包含,减少与宿主的隐式耦合。
优先考虑组合(把能力做成独立对象注入)而非混入。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 横向复用能力、绕开单继承限制 |

| 最适用场景 | 多能力叠加、跨类复用 |

| 核心收益 | 灵活组合能力 |

| 主要代价 | 命名冲突、来源难追踪 |

JavaScript 特色模式:发布订阅

概念与类比

发布订阅(Publish-Subscribe)通过一个事件中心(Event Bus)解耦消息的发布者和订阅者,二者互不知道对方的存在。

类比报社订阅:读者向报社订阅报纸,报社出新报就派送给所有订阅者。读者不认识写稿的记者,记者也不知道有哪些读者——报社(事件中心)居中调度。

发布订阅 vs 观察者

这两个概念常被混用,但有关键区别:观察者模式中,主题(Subject)直接持有观察者列表并直接通知它们;发布订阅中,发布者和订阅者完全不知道对方,通过第三方事件中心(Broker)通信,耦合更松。

| 维度 | 观察者模式 | 发布订阅 |

| --- | --- | --- |

| 双方是否知道彼此 | 主题知道观察者 | 完全不知道 |

| 是否有中间人 | 无(直接通知) | 有(事件中心) |

| 耦合度 | 较松 | 最松 |

| 典型实现 | Subject.notify() | EventBus.emit() |

代码示例:完整事件中心

javascriptCode
class EventBus {
  constructor() {
    this.events = new Map();
  }

  on(event, handler) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }
    this.events.get(event).add(handler);
    // 返回取消订阅函数
    return () => this.off(event, handler);
  }

  once(event, handler) {
    const wrapper = (...args) => {
      handler(...args);
      this.off(event, wrapper);
    };
    return this.on(event, wrapper);
  }

  off(event, handler) {
    const handlers = this.events.get(event);
    if (handlers) {
      handlers.delete(handler);
      if (handlers.size === 0) this.events.delete(event);
    }
  }

  emit(event, ...args) {
    const handlers = this.events.get(event);
    if (handlers) {
      // 复制一份避免遍历时修改
      [...handlers].forEach((handler) => {
        try {
          handler(...args);
        } catch (e) {
          console.error(`事件 ${event} 处理出错:`, e);
        }
      });
    }
  }
}

const bus = new EventBus();

const unsub = bus.on('login', (user) => console.log('监听器A:', user.name));
bus.on('login', (user) => console.log('监听器B:', user.name));
bus.once('login', (user) => console.log('只触发一次:', user.name));

bus.emit('login', { name: 'Alice' }); // A、B、once 都触发
bus.emit('login', { name: 'Bob' });   // A、B 触发,once 不再触发

unsub(); // 取消监听器 A
bus.emit('login', { name: 'Carol' }); // 只有 B 触发

常见坑

内存泄漏:订阅后不取消,尤其组件卸载时忘记 off,是前端头号内存泄漏来源。
事件命名混乱:字符串事件名无类型检查,拼写错误静默失败。建议用常量集中管理。
调试困难:过度解耦导致"某个事件到底谁在监听"难以追踪。

最佳实践

订阅时保存取消函数,组件销毁时务必取消。
事件名用常量集中定义,避免魔法字符串。
不要滥用全局事件总线,会让数据流变得难以追踪。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 通过事件中心解耦发布者与订阅者 |

| 最适用场景 | 跨组件通信、事件系统、消息广播 |

| 核心收益 | 最大程度解耦 |

| 主要代价 | 内存泄漏风险、数据流难追踪 |

JavaScript 特色模式:依赖注入

概念与类比

依赖注入(Dependency Injection,DI)不在对象内部自己创建依赖,而是从外部把依赖"注入"进来,实现控制反转(IoC)。

类比装修房子:与其让房子自己去生产家具(内部 new),不如你把选好的家具搬进去(注入)。这样想换家具时不用拆房子,只需搬进新家具。

为什么重要

DI 是可测试性低耦合的关键。当一个类的依赖从外部注入时,测试时就能轻松传入 mock 依赖;替换实现时也无需改动类本身。Angular、NestJS 的核心就是 DI 容器。

代码示例:手动依赖注入

javascriptCode
// 不好:内部硬编码依赖,无法替换、难以测试
class BadUserService {
  constructor() {
    this.db = new MySQLDatabase(); // 硬编码,死板
  }
}

// 好:依赖从外部注入
class UserService {
  constructor(database, logger) {
    this.db = database;
    this.logger = logger;
  }
  getUser(id) {
    this.logger.log(`查询用户 ${id}`);
    return this.db.query(id);
  }
}

// 生产环境注入真实依赖
const realDb = { query: (id) => ({ id, name: 'Alice' }) };
const realLogger = { log: (msg) => console.log(msg) };
const service = new UserService(realDb, realLogger);
console.log(service.getUser(1));

// 测试环境注入 mock,轻松隔离
const mockDb = { query: (id) => ({ id, name: 'TestUser' }) };
const mockLogger = { log: () => {} }; // 静默
const testService = new UserService(mockDb, mockLogger);
console.log(testService.getUser(99));

代码示例二:简易 DI 容器

javascriptCode
class Container {
  constructor() {
    this.registry = new Map();
    this.singletons = new Map();
  }

  register(name, factory, { singleton = false } = {}) {
    this.registry.set(name, { factory, singleton });
  }

  resolve(name) {
    const entry = this.registry.get(name);
    if (!entry) throw new Error(`未注册的依赖: ${name}`);

    if (entry.singleton) {
      if (!this.singletons.has(name)) {
        this.singletons.set(name, entry.factory(this));
      }
      return this.singletons.get(name);
    }
    return entry.factory(this);
  }
}

const container = new Container();
container.register('logger', () => ({ log: (m) => console.log('[LOG]', m) }), { singleton: true });
container.register('db', () => ({ query: (id) => ({ id }) }), { singleton: true });
container.register('userService', (c) => new UserService(c.resolve('db'), c.resolve('logger')));

const svc = container.resolve('userService');
console.log(svc.getUser(42));

最佳实践

依赖通过构造函数注入,让依赖关系显式化。
大型项目可用 DI 容器统一管理对象生命周期。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 从外部注入依赖,实现控制反转 |

| 最适用场景 | 可测试性要求高、大型应用 |

| 核心收益 | 低耦合、易测试、易替换 |

| 主要代价 | 需要容器或手动接线 |

JavaScript 特色模式:柯里化与函数组合

柯里化 Currying

柯里化把一个接受多个参数的函数,转换成一系列每次只接受一个参数的函数。它让函数可以"分步传参",是函数复用和偏应用的利器。

javascriptCode
// 通用柯里化工具
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...next) => curried.apply(this, args.concat(next));
  };
}

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

// 实用场景:固定部分参数生成专用函数
const log = curry((level, module, message) =>
  console.log(`[${level}][${module}] ${message}`)
);
const errorLog = log('ERROR');
const authError = errorLog('Auth');
authError('登录失败'); // [ERROR][Auth] 登录失败

函数组合 compose 与 pipe

函数组合把多个单一职责的小函数串联成一个大函数。`compose` 从右向左执行,`pipe` 从左向右执行(更符合阅读习惯)。

javascriptCode
// compose: 从右到左
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
// pipe: 从左到右
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const trim = (s) => s.trim();
const toLower = (s) => s.toLowerCase();
const removeSpaces = (s) => s.replace(/\s+/g, '-');

// 生成 URL slug
const slugify = pipe(trim, toLower, removeSpaces);
console.log(slugify('  Hello World Foo  ')); // hello-world-foo

// compose 顺序相反
const slugify2 = compose(removeSpaces, toLower, trim);
console.log(slugify2('  Hello World  ')); // hello-world

compose vs pipe 对比

| 维度 | compose | pipe |

| --- | --- | --- |

| 执行方向 | 右 → 左 | 左 → 右 |

| 数学直觉 | 符合 f(g(x)) | 反直觉 |

| 阅读直觉 | 需倒着读 | 顺着读 |

| 代表库 | Redux compose | RxJS pipe、Lodash flow |

最佳实践

保持每个小函数单一职责、纯函数(无副作用)。
团队约定优先用 `pipe`,阅读顺序更自然。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 柯里化分步传参、组合串联小函数 |

| 最适用场景 | 数据管道、函数复用、偏应用 |

| 核心收益 | 高复用、可读性强、易测试 |

| 主要代价 | 过度组合会降低可调试性 |

JavaScript 特色模式:防抖节流与记忆化

防抖与节流作为策略

防抖(debounce)和节流(throttle)本质是控制函数执行频率的两种"策略",常和策略模式配合,根据场景选择。

javascriptCode
// 防抖:事件停止 delay 毫秒后才执行(只关心最后一次)
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 节流:每 interval 毫秒最多执行一次(均匀执行)
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

// 策略选择表
const rateLimitStrategies = {
  search: (fn) => debounce(fn, 300),      // 搜索框:停止输入才搜
  scroll: (fn) => throttle(fn, 100),      // 滚动:均匀触发
  resize: (fn) => debounce(fn, 200),      // 窗口缩放:结束才处理
  buttonClick: (fn) => throttle(fn, 1000),// 防重复提交
};

const onSearch = rateLimitStrategies.search((q) => console.log('搜索:', q));
onSearch('a'); onSearch('ab'); onSearch('abc'); // 只有最后一次 300ms 后执行

防抖 vs 节流对比

| 场景 | 选择 | 触发时机 | 典型延迟 |

| --- | --- | --- | --- |

| 搜索输入联想 | 防抖 | 停止输入后 | 300ms |

| 滚动加载/视差 | 节流 | 固定间隔 | 100-200ms |

| 窗口 resize | 防抖 | 停止调整后 | 200ms |

| 按钮防重复点击 | 节流 | 首次立即 | 1000ms |

| 拖拽 | 节流 | 固定间隔 | 16ms(60fps) |

记忆化 Memoize

记忆化缓存函数计算结果,相同输入直接返回缓存,避免重复计算。是空间换时间的经典优化。

javascriptCode
function memoize(fn, resolver) {
  const cache = new Map();
  return function (...args) {
    const key = resolver ? resolver(...args) : JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// 未优化的斐波那契:指数级时间复杂度 O(2^n)
function slowFib(n) {
  return n < 2 ? n : slowFib(n - 1) + slowFib(n - 2);
}

// 记忆化后:O(n)
const fastFib = memoize(function fib(n) {
  return n < 2 ? n : fastFib(n - 1) + fastFib(n - 2);
});

console.log(fastFib(40)); // 瞬间返回

记忆化性能对比

| n | 未记忆化(O(2^n)) 调用次数 | 记忆化(O(n)) 调用次数 | 提速 |

| --- | --- | --- | --- |

| 20 | 约 13529 次 | 20 次 | 约 676 倍 |

| 30 | 约 1664079 次 | 30 次 | 约 55469 倍 |

| 40 | 约 2 亿次 | 40 次 | 数百万倍 |

记忆化常见坑

缓存无限增长:key 种类无限时缓存吃光内存,需用 LRU 或 WeakMap 限制。
参数序列化开销:`JSON.stringify` 做 key 对复杂对象慢且不可靠,建议自定义 resolver。
不适合有副作用的函数:记忆化只对纯函数安全。

最佳实践

只记忆化纯函数、且计算成本 > 缓存成本时才用。
大缓存用 LRU 淘汰策略防止内存泄漏。
对象参数用 WeakMap 做 key 缓存,可自动垃圾回收。

小结

| 要点 | 说明 |

| --- | --- |

| 防抖 | 只执行最后一次,适合搜索、resize |

| 节流 | 固定频率执行,适合滚动、拖拽 |

| 记忆化 | 缓存结果,空间换时间 |

| 共同点 | 都用闭包保存状态 |

现代框架中的设计模式:React

React 是设计模式的集大成者。理解 React 的各种"模式",本质就是理解经典设计模式在函数式 UI 里的变体。

高阶组件(HOC)= 装饰器模式

高阶组件接收一个组件,返回一个增强后的新组件,接口保持不变——这正是装饰器模式的定义。

javascriptCode
// HOC:给组件添加加载状态能力(装饰器)
function withLoading(WrappedComponent) {
  return function WithLoading(props) {
    if (props.isLoading) {
      return '加载中...'; // 伪代码,实际返回 JSX
    }
    return WrappedComponent(props);
  };
}

// HOC:添加日志能力,可叠加多个装饰器
function withLogger(WrappedComponent) {
  return function WithLogger(props) {
    console.log('渲染组件, props:', props);
    return WrappedComponent(props);
  };
}

const UserList = (props) => `用户列表: ${props.users.join(', ')}`;

// 层层装饰,和结构型装饰器模式一模一样
const Enhanced = withLogger(withLoading(UserList));

Render Props = 策略模式

Render Props 把"如何渲染"作为一个函数参数传入组件,组件负责逻辑,渲染策略由外部注入——这就是策略模式。

javascriptCode
// 组件只管获取数据(逻辑),怎么渲染由 render 策略决定
function DataFetcher({ data, render }) {
  // 组件封装数据获取逻辑
  return render(data); // 渲染策略外部注入
}

// 同一份数据,注入不同渲染策略
DataFetcher({ data: [1, 2, 3], render: (d) => `列表: ${d.join(',')}` });
DataFetcher({ data: [1, 2, 3], render: (d) => `总和: ${d.reduce((a, b) => a + b)}` });

自定义 Hooks = 组合模式 + 策略

Hooks 用组合替代继承,把可复用的有状态逻辑抽成函数,多个 Hook 自由组合。

javascriptCode
// 自定义 Hook:封装可复用逻辑(伪代码演示思路)
function useToggle(initial = false) {
  // 实际会用 useState
  let state = initial;
  const toggle = () => { state = !state; };
  return [state, toggle];
}

function useCounter(initial = 0) {
  let count = initial;
  return {
    count,
    increment: () => count++,
    decrement: () => count--,
  };
}

// 一个组件里组合多个 Hook,各自独立、互不干扰
function useMyComponent() {
  const [isOpen, toggleOpen] = useToggle();
  const counter = useCounter(10);
  return { isOpen, toggleOpen, counter };
}

Context = 依赖注入

React Context 让深层组件无需层层传 props 就能拿到依赖,正是依赖注入的思想——依赖从"提供者"注入到消费树。

useReducer / Redux = 命令模式

派发的 action 就是命令对象(`{ type, payload }`),reducer 是命令的执行者。所有状态变更都通过命令描述,可记录、可回放、可时间旅行调试。

javascriptCode
// action = 命令对象
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + (action.payload || 1) };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      return state;
  }
}

let state = { count: 0 };
// dispatch 命令
state = reducer(state, { type: 'INCREMENT', payload: 5 });
console.log(state); // { count: 5 }
state = reducer(state, { type: 'RESET' });
console.log(state); // { count: 0 }

React 模式对照表

| React 概念 | 对应经典模式 | 核心思想 |

| --- | --- | --- |

| 高阶组件 HOC | 装饰器 | 包装增强、接口不变 |

| Render Props | 策略 | 渲染逻辑外部注入 |

| 自定义 Hooks | 组合 | 逻辑复用、组合优于继承 |

| Context | 依赖注入 | 依赖从上向下注入 |

| useReducer/Redux | 命令 | action 是命令对象 |

| 事件系统 | 观察者 | 订阅通知 |

现代框架中的设计模式:Vue 与 Redux

Vue 响应式 = 观察者 + 代理模式

Vue 3 的响应式系统用 `Proxy` 拦截对象的读写:读取时收集依赖(订阅),写入时通知更新(发布)。这是观察者模式和代理模式的完美结合。

javascriptCode
// 极简版 Vue3 响应式原理
const effectStack = [];

function reactive(obj) {
  const deps = new Map(); // key -> Set<effect>

  return new Proxy(obj, {
    get(target, key) {
      // 依赖收集(订阅)
      const activeEffect = effectStack[effectStack.length - 1];
      if (activeEffect) {
        if (!deps.has(key)) deps.set(key, new Set());
        deps.get(key).add(activeEffect);
      }
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      // 派发更新(发布通知)
      const effects = deps.get(key);
      if (effects) {
        effects.forEach((effect) => effect());
      }
      return true;
    },
  });
}

function watchEffect(fn) {
  effectStack.push(fn);
  fn(); // 首次执行触发依赖收集
  effectStack.pop();
}

const state = reactive({ count: 0 });
watchEffect(() => {
  console.log('count 变了:', state.count);
});
state.count = 1; // 自动触发:count 变了: 1
state.count = 2; // 自动触发:count 变了: 2

Redux = 单向数据流 + 中间件责任链

Redux 的中间件机制是责任链模式的典范:每个中间件处理 action 后决定是否传给下一个。

javascriptCode
// Redux applyMiddleware 简化实现(责任链 + 洋葱模型)
function applyMiddleware(...middlewares) {
  return (createStore) => (reducer) => {
    const store = createStore(reducer);
    let dispatch = store.dispatch;

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action),
    };

    // 每个中间件包装 dispatch,形成责任链
    const chain = middlewares.map((mw) => mw(middlewareAPI));
    dispatch = chain.reduceRight((next, mw) => mw(next), store.dispatch);

    return { ...store, dispatch };
  };
}

// 日志中间件
const logger = (store) => (next) => (action) => {
  console.log('派发前:', action.type);
  const result = next(action); // 传给下一个中间件(责任链)
  console.log('派发后 state:', store.getState());
  return result;
};

// 异步中间件(thunk)
const thunk = (store) => (next) => (action) => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

Express/Koa 中间件 = 责任链洋葱模型

Express 的 `app.use` 和 Koa 的 async 中间件都是责任链。Koa 的洋葱模型尤其经典:请求先由外到内穿过每层中间件的前半段,到达核心后再由内到外穿过后半段。

javascriptCode
// Express 风格中间件责任链
class MiniExpress {
  constructor() {
    this.middlewares = [];
  }
  use(fn) {
    this.middlewares.push(fn);
    return this;
  }
  handle(req, res) {
    let index = 0;
    const next = () => {
      const mw = this.middlewares[index++];
      if (mw) mw(req, res, next); // 显式调用 next 传递
    };
    next();
  }
}

const app = new MiniExpress();
app.use((req, res, next) => { console.log('日志中间件'); next(); });
app.use((req, res, next) => { req.user = 'Alice'; next(); });
app.use((req, res) => { console.log('业务处理, 用户:', req.user); });

app.handle({}, {});

框架模式总览

| 框架机制 | 设计模式 | 说明 |

| --- | --- | --- |

| Vue 响应式 | 观察者 + 代理 | Proxy 拦截,依赖收集派发 |

| Vue mixin/组合式 | 混入/组合 | 逻辑复用 |

| Redux store | 单例 | 全局唯一状态树 |

| Redux action | 命令 | 描述状态变更 |

| Redux middleware | 责任链 | 层层处理 dispatch |

| Express/Koa 中间件 | 责任链 | 洋葱模型 |

| RxJS | 观察者 + 迭代器 | 响应式流 |

反模式与重构

学会识别反模式(Anti-Pattern)和过度设计,比学会更多模式更重要。设计模式用错地方,危害远大于不用。

过度设计(Over-Engineering)

为了不存在或极不可能出现的需求,提前引入复杂抽象。

javascriptCode
// 反模式:一个简单加法被过度抽象
class AbstractCalculationStrategyFactory {
  createStrategy(type) { /* ... */ }
}
class AdditionStrategyImpl { execute(a, b) { return a + b; } }
// ...几百行框架代码,只为算 1 + 1

// 重构:需求就这么简单,一个函数搞定
const add = (a, b) => a + b;

判断标准(YAGNI 原则:You Aren't Gonna Need It):如果当前没有明确的多变化点需求,就不要提前抽象。等到真正出现第二、第三个变体时再重构成模式。

上帝对象(God Object)

一个类/模块承担了过多职责,无所不知、无所不能,牵一发动全身。

javascriptCode
// 反模式:一个类什么都干
class AppManager {
  login() {}
  logout() {}
  fetchUsers() {}
  renderUI() {}
  sendEmail() {}
  processPayment() {}
  generateReport() {}
  connectDatabase() {}
  // ...还有 50 个方法
}

// 重构:按职责拆分(单一职责原则)
class AuthService { login() {} logout() {} }
class UserService { fetchUsers() {} }
class PaymentService { processPayment() {} }
class EmailService { sendEmail() {} }

单例滥用

把单例当全局变量用,任何地方都能改它的状态,导致隐式耦合和测试困难。

javascriptCode
// 反模式:全局单例被到处直接修改
const GlobalState = { user: null, cart: [], theme: 'light', /* 什么都往里塞 */ };
// 任何模块都能改,出了 bug 无从追踪

// 重构:用依赖注入 + 明确的状态管理
// 状态变更走统一入口(如 Redux dispatch),可追踪、可测试

单例的问题:全局状态使单元测试互相污染(一个测试改了单例,影响另一个);隐藏依赖关系(看构造函数看不出它依赖了单例);并发环境下有竞态。能用依赖注入就别用单例。

常见反模式速查表

| 反模式 | 症状 | 重构方向 |

| --- | --- | --- |

| 过度设计 | 简单需求套复杂框架 | YAGNI,删抽象 |

| 上帝对象 | 一个类几十个方法 | 单一职责拆分 |

| 单例滥用 | 全局可变状态到处改 | 依赖注入 + 状态管理 |

| 复制粘贴编程 | 大量重复代码 | 提取函数/模板方法 |

| 意大利面代码 | 逻辑纠缠无结构 | 分层 + 模式重构 |

| 魔法数字/字符串 | 硬编码字面量 | 常量/枚举 |

| 回调地狱 | 深层嵌套回调 | Promise/async、责任链 |

| 过早优化 | 没测就优化 | 先测量再优化 |

重构的时机与信号

| 代码坏味道 | 可能适用的模式 |

| --- | --- |

| 大量 if-else 判类型 | 策略、状态、多态 |

| new 散落各处 | 工厂、依赖注入 |

| 重复的算法骨架 | 模板方法 |

| 对象间网状引用 | 中介者 |

| 需要撤销/重做 | 命令 + 备忘录 |

| 第三方接口不匹配 | 适配器 |

| 需要事后增强功能 | 装饰器 |

| 树形结构处理 | 组合、访问者 |

重构原则

先测试后重构:重构前确保有测试覆盖,重构后测试仍通过,保证行为不变。
小步前进:每次只做一个小改动,随时可回退。
模式是重构的终点,不是起点:不要一开始就照模式写,而是当代码坏味道出现时,用模式去消除它。这是 Martin Fowler《重构》的核心思想。

反模式小结

| 要点 | 说明 |

| --- | --- |

| 核心教训 | 用错模式比不用更糟 |

| 黄金原则 | YAGNI + KISS + 单一职责 |

| 重构方向 | 从坏味道出发,用模式消除 |

| 前提条件 | 充分的测试覆盖 |

综合对比:23 种 GoF 模式速查

| 模式 | 类型 | 一句话意图 | 前端典型场景 |

| --- | --- | --- | --- |

| 单例 | 创建型 | 全局唯一实例 | 全局配置、缓存、store |

| 工厂方法 | 创建型 | 延迟对象创建到子类 | 创建不同类型组件 |

| 抽象工厂 | 创建型 | 创建产品族 | 跨平台 UI、多主题 |

| 建造者 | 创建型 | 分步构建复杂对象 | 查询构造、请求配置 |

| 原型 | 创建型 | 克隆已有对象 | 深浅拷贝、对象复制 |

| 适配器 | 结构型 | 转换不兼容接口 | 第三方 SDK、数据格式 |

| 桥接 | 结构型 | 分离抽象与实现 | 多维度变化 |

| 组合 | 结构型 | 树形统一处理 | DOM、组件树、菜单 |

| 装饰器 | 结构型 | 动态增强功能 | HOC、中间件增强 |

| 外观 | 结构型 | 简化子系统入口 | SDK 封装、utils |

| 享元 | 结构型 | 共享细粒度对象 | 地图标记、粒子 |

| 代理 | 结构型 | 控制对象访问 | 懒加载、Vue 响应式 |

| 责任链 | 行为型 | 请求沿链传递 | 中间件、审批流 |

| 命令 | 行为型 | 请求封装成对象 | undo/redo、action |

| 解释器 | 行为型 | 解释小语言 | 规则引擎、DSL |

| 迭代器 | 行为型 | 统一遍历集合 | for...of、生成器 |

| 中介者 | 行为型 | 中心协调交互 | 组件联动、聊天室 |

| 备忘录 | 行为型 | 保存恢复状态 | 快照、草稿、存档 |

| 观察者 | 行为型 | 一对多通知 | 事件系统、响应式 |

| 状态 | 行为型 | 状态改变行为 | 订单、播放器 |

| 策略 | 行为型 | 算法可替换 | 表单校验、支付方式 |

| 模板方法 | 行为型 | 定义算法骨架 | 生命周期、流程复用 |

| 访问者 | 行为型 | 操作外置于结构 | AST、编译器 |

学习设计模式的路线建议

1.第一阶段:识别。先能在框架源码和自己的代码里认出模式("哦,这里是观察者")。多读 React、Vue、Redux、Koa 源码。
2.第二阶段:应用。在真实项目中,当遇到对应的坏味道时,主动用模式重构。切忌为了用而用。
3.第三阶段:融会贯通。理解模式背后的 SOLID 原则和"组合优于继承""面向接口编程"等思想,做到不拘泥于具体模式形态,灵活运用。
4.第四阶段:知道何时不用。最高境界是知道什么时候不该用模式——保持简单,YAGNI,让代码恰如其分。

学习阶段小结

| 阶段 | 目标 | 关键动作 |

| --- | --- | --- |

| 识别 | 看懂模式 | 读框架源码 |

| 应用 | 会用模式 | 重构真实代码 |

| 融会 | 理解本质 | 掌握 SOLID |

| 克制 | 知道不用 | 保持简单 |

结构型模式深入:代理 Proxy

概念与类比

代理模式(Proxy)为另一个对象提供一个替身或占位符,以控制对这个对象的访问。

类比明星经纪人:你想找明星拍广告,不能直接联系明星,得先过经纪人这一关。经纪人(代理)帮明星(真实对象)筛选请求、谈价格、挡掉骚扰,只有合适的请求才转达给明星。代理和真实对象接口一致,你甚至感觉不到中间隔了一层。

为什么重要

JavaScript 的 `Proxy` 是语言级的代理支持,是 Vue 3 响应式、MobX、各种数据校验和拦截库的底层基础。代理模式常用于:懒加载(虚拟代理)、访问控制(保护代理)、缓存(缓存代理)、日志埋点、远程调用等横切关注点。

代理的分类

| 类型 | 用途 | 例子 |

| --- | --- | --- |

| 虚拟代理 | 延迟创建昂贵对象 | 图片懒加载 |

| 保护代理 | 控制访问权限 | 权限校验 |

| 缓存代理 | 缓存结果 | 记忆化 |

| 远程代理 | 代理远程对象 | RPC、API 客户端 |

| 日志代理 | 记录访问 | 埋点、审计 |

代码示例一:虚拟代理实现图片懒加载

javascriptCode
// 真实对象:加载真实图片(昂贵操作)
class RealImage {
  constructor(url) {
    this.url = url;
    this.loadFromDisk(); // 构造即加载,昂贵
  }
  loadFromDisk() {
    console.log(`从网络加载大图: ${this.url}`);
  }
  display() {
    console.log(`显示图片: ${this.url}`);
  }
}

// 虚拟代理:延迟到真正需要时才加载
class ImageProxy {
  constructor(url) {
    this.url = url;
    this.realImage = null; // 先不加载
  }
  display() {
    if (!this.realImage) {
      this.realImage = new RealImage(this.url); // 首次 display 才加载
    }
    this.realImage.display();
  }
}

const img = new ImageProxy('huge-photo.jpg'); // 此时未加载,很快
console.log('代理已创建,尚未加载');
img.display(); // 此时才真正加载并显示
img.display(); // 第二次直接用缓存,不重复加载

代码示例二:用原生 Proxy 实现数据校验

javascriptCode
function createValidatedUser(target) {
  const validators = {
    age(value) {
      if (typeof value !== 'number' || value < 0 || value > 150) {
        throw new Error(`age 非法: ${value}`);
      }
    },
    email(value) {
      if (!/^[^@]+@[^@]+$/.test(value)) {
        throw new Error(`email 非法: ${value}`);
      }
    },
  };

  return new Proxy(target, {
    set(obj, prop, value) {
      if (validators[prop]) {
        validators[prop](value); // 拦截写入做校验
      }
      obj[prop] = value;
      return true;
    },
  });
}

const user = createValidatedUser({});
user.age = 30; // 通过
user.email = 'a@b.com'; // 通过
try {
  user.age = -5; // 抛错
} catch (e) {
  console.log('拦截到非法赋值:', e.message);
}

代码示例三:缓存代理

javascriptCode
// 用 Proxy 给任意函数加缓存能力
function cacheProxy(fn) {
  const cache = new Map();
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        console.log('命中缓存');
        return cache.get(key);
      }
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    },
  });
}

const expensiveCalc = cacheProxy((a, b) => {
  console.log('执行昂贵计算');
  return a * b + Math.random();
});

const r1 = expensiveCalc(2, 3); // 执行昂贵计算
const r2 = expensiveCalc(2, 3); // 命中缓存
console.log(r1 === r2); // true

代理 vs 装饰器 vs 适配器

| 维度 | 代理 | 装饰器 | 适配器 |

| --- | --- | --- | --- |

| 目的 | 控制访问 | 增强功能 | 转换接口 |

| 接口变化 | 不变 | 不变 | 改变 |

| 是否知道真实对象 | 是(常自己创建) | 是(外部传入) | 是 |

| 典型 | 懒加载、权限 | 叠加职责 | 兼容接口 |

常见坑

Proxy 性能开销:每次属性访问都过一层拦截,热路径中大量访问有性能成本(通常比直接访问慢数倍)。
无法代理某些内部方法:如私有字段 `#`、部分内置对象行为。
this 绑定问题:代理内部方法时注意 `this` 指向。

最佳实践

用代理处理横切关注点(日志、缓存、校验、权限),保持核心对象纯净。
性能敏感的热路径慎用 Proxy,评估开销。
优先用原生 `Proxy` + `Reflect` 实现,语义清晰。

小结

| 要点 | 说明 |

| --- | --- |

| 一句话定义 | 为对象提供替身以控制访问 |

| 最适用场景 | 懒加载、权限、缓存、响应式 |

| 核心收益 | 无侵入地插入横切逻辑 |

| 主要代价 | 性能开销、部分场景受限 |

综合实战:一个模式组合的真实案例

真实项目中很少单独使用一个模式,而是多个模式协同。下面用一个"可撤销的绘图应用"串联多个模式。

javascriptCode
// 单例:全局绘图上下文
class DrawingContext {
  static instance = null;
  static getInstance() {
    if (!this.instance) this.instance = new DrawingContext();
    return this.instance;
  }
  constructor() {
    this.shapes = [];
  }
}

// 工厂:创建不同图形
class ShapeFactory {
  static create(type, config) {
    const creators = {
      circle: (c) => ({ type: 'circle', ...c }),
      rect: (c) => ({ type: 'rect', ...c }),
    };
    return creators[type](config);
  }
}

// 命令 + 备忘录:可撤销的绘图操作
class AddShapeCommand {
  constructor(context, shape) {
    this.context = context;
    this.shape = shape;
  }
  execute() {
    this.context.shapes.push(this.shape);
  }
  undo() {
    this.context.shapes.pop();
  }
}

// 观察者:绘图变化通知 UI 刷新
class DrawingApp {
  constructor() {
    this.context = DrawingContext.getInstance();
    this.history = [];
    this.listeners = [];
  }
  onChange(fn) {
    this.listeners.push(fn);
  }
  notify() {
    this.listeners.forEach((fn) => fn(this.context.shapes));
  }
  draw(type, config) {
    const shape = ShapeFactory.create(type, config); // 工厂
    const command = new AddShapeCommand(this.context, shape); // 命令
    command.execute();
    this.history.push(command);
    this.notify(); // 观察者
  }
  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.notify();
    }
  }
}

const app = new DrawingApp();
app.onChange((shapes) => console.log('画布更新,图形数:', shapes.length));
app.draw('circle', { x: 10, y: 10, r: 5 }); // 画布更新,图形数: 1
app.draw('rect', { x: 0, y: 0, w: 20, h: 10 }); // 画布更新,图形数: 2
app.undo(); // 画布更新,图形数: 1

这个例子中:单例保证全局上下文唯一,工厂封装图形创建,命令封装可撤销操作,备忘录思想用于状态回退,观察者负责通知 UI。这正是设计模式在真实工程中的组合运用方式。

组合运用小结

| 模式 | 在案例中的角色 |

| --- | --- |

| 单例 | 全局绘图上下文 |

| 工厂 | 统一创建图形对象 |

| 命令 | 封装可撤销的绘图操作 |

| 备忘录 | 支撑撤销时的状态回退 |

| 观察者 | 数据变化通知 UI 刷新 |

最佳实践

设计模式使用原则:

理解设计模式的意图和适用场景,避免为了使用模式而使用模式
选择适合的设计模式,根据具体问题选择最合适的解决方案
避免过度设计,简单问题用简单方案解决
结合 JavaScript 的特性,利用闭包、原型链、高阶函数等特性简化实现
javascriptCode
// 过度设计的例子
class SingletonFactoryBuilder {
  // 不必要的复杂性
}

// 简洁的实现
const singleton = {
  instance: null,
  getInstance() {
    if (!this.instance) {
      this.instance = { data: {} };
    }
    return this.instance;
  },
};

代码组织与可维护性:

保持代码简洁,设计模式应该简化代码而不是增加复杂性
注重可维护性,代码应该易于理解和修改
学习设计模式的变体,根据实际需求灵活调整
实践中积累经验,在真实项目中应用和体会设计模式
javascriptCode
// 模块化组织设计模式
// patterns/observer.js
export class EventEmitter { /* ... */ }

// patterns/strategy.js
export const strategies = { /* ... */ }

// patterns/factory.js
export class Factory { /* ... */ }

// 使用
import { EventEmitter } from './patterns/observer';
import { strategies } from './patterns/strategy';

性能考虑:

观察者模式中注意内存泄漏,及时取消订阅
代理模式中注意性能开销,避免过度拦截
迭代器模式中注意内存使用,大数据集使用生成器
javascriptCode
// 避免内存泄漏
class Component {
  constructor() {
    this.unsubscribe = store.subscribe(this.handleChange);
  }
  
  handleChange = (state) => {
    // 处理状态变化
  }
  
  destroy() {
    // 清理订阅
    this.unsubscribe();
  }
}

// 使用 WeakMap 避免内存泄漏
const privateData = new WeakMap();

class MyClass {
  constructor() {
    privateData.set(this, { secret: 'value' });
  }
  
  getSecret() {
    return privateData.get(this).secret;
  }
}

测试设计模式:

单元测试每个模式的核心功能
测试模式的边界情况
测试模式之间的交互
javascriptCode
// 测试观察者模式
describe('EventEmitter', () => {
  it('should call listeners when event is emitted', () => {
    const emitter = new EventEmitter();
    const listener = jest.fn();
    
    emitter.on('test', listener);
    emitter.emit('test', 'data');
    
    expect(listener).toHaveBeenCalledWith('data');
  });
  
  it('should unsubscribe correctly', () => {
    const emitter = new EventEmitter();
    const listener = jest.fn();
    
    const unsubscribe = emitter.on('test', listener);
    unsubscribe();
    emitter.emit('test', 'data');
    
    expect(listener).not.toHaveBeenCalled();
  });
});