JavaScript 设计模式与应用
JavaScript 设计模式与应用
设计模式是解决软件设计中常见问题的可重用方案。在 JavaScript 中,设计模式不仅可以帮助我们编写更加优雅、可维护的代码,还能提高代码的可读性和可扩展性。JavaScript 的动态特性使得某些设计模式的实现与传统面向对象语言有所不同,理解这些差异对于正确应用设计模式至关重要。
为什么要学习设计模式
设计模式最早由 GoF(Gang of Four,四人组)在《设计模式:可复用面向对象软件的基础》一书中系统总结,共 23 种经典模式。虽然这本书以 C++ 和 Smalltalk 为例,但其思想是跨语言的。对于 JavaScript 开发者而言,设计模式的价值主要体现在以下几个方面:
需要强调的是:设计模式不是银弹,也不是越多越好。滥用设计模式(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,你不会需要它)以及迪米特法则(最少知识原则,一个对象应尽量少地了解其他对象)。这些原则共同指导我们判断"何时该用模式、何时不该用"。
// 违反开闭原则的写法:每新增一种支付方式都要改这个函数
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 中,单例模式常用于管理全局状态、配置对象、数据库连接池等场景。实现单例模式的关键是控制实例的创建过程,确保多次调用构造函数时返回同一个实例。
// 单例模式实现
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。
为什么单例模式重要:
单例模式的结构(文字版 UML):
单例类内部持有一个指向自身唯一实例的静态引用(instance),构造函数被"私有化"或加以控制,外部只能通过静态方法 getInstance() 获取实例。第一次调用时创建并缓存实例(懒加载),之后每次调用都返回缓存的实例。结构可以概括为:Singleton 类 —持有→ 静态 instance 字段;Singleton 类 —暴露→ getInstance() 静态方法。
更多代码变体:
// 变体一:闭包 + 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,两次拿到的是同一实例// 变体二:使用 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 } 拿到的都是同一个引用,无需任何额外代码// 变体四:惰性单例的通用封装(提取"单例"这一横切逻辑)
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真实案例:
单例与相近模式对比:
| 对比项 | 单例模式 | 静态工具类 | 全局变量 |
| --- | --- | --- | --- |
| 是否可延迟初始化 | 是(懒加载) | 否 | 否 |
| 是否可实现接口/多态 | 是 | 否 | 否 |
| 是否可被继承扩展 | 较难 | 否 | 否 |
| 命名空间污染风险 | 低 | 低 | 高 |
| 单元测试友好度 | 中(需重置实例) | 中 | 低 |
常见坑:
最佳实践:
工厂模式(Factory):
工厂模式封装对象的创建过程,提供统一的创建接口,隐藏具体实现细节。工厂模式分为简单工厂、工厂方法和抽象工厂三种。在 JavaScript 中,工厂模式常用于创建复杂对象、根据条件创建不同类型的对象等场景。
// 简单工厂
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) | 提供创建一系列相关对象的接口,不指定具体类 | 高 | 一个工厂创建一整套配套产品(按钮+复选框+对话框) |
为什么工厂模式重要:
工厂模式的结构(文字版 UML):
以工厂方法为例:抽象产品接口 Product 定义产品的行为;具体产品 ConcreteProductA、ConcreteProductB 实现该接口;抽象创建者 Creator 声明工厂方法 factoryMethod()(返回 Product 类型);具体创建者 ConcreteCreatorA、ConcreteCreatorB 覆写 factoryMethod() 分别返回不同的具体产品。关系为:Creator —依赖→ Product;ConcreteCreator —创建→ ConcreteProduct。
更多代码变体:
// 变体一:用注册表替代 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', ...),无需修改工厂内部// 变体二:函数式简单工厂——返回不同的对象字面量
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']// 变体三:抽象工厂——创建一整套主题化的 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); // 一整套组件风格保持一致真实案例:
工厂与相近模式对比:
| 对比项 | 简单工厂 | 工厂方法 | 抽象工厂 | 建造者模式 |
| --- | --- | --- | --- | --- |
| 关注点 | 创建单个产品 | 创建单个产品,延迟到子类 | 创建产品族 | 分步骤构建复杂对象 |
| 是否符合开闭原则 | 否(改 switch) | 是 | 是 | 是 |
| 客户端感知具体类 | 感知 | 不感知 | 不感知 | 不感知 |
| 适用场景 | 类型少且稳定 | 类型可能扩展 | 多套配套产品 | 参数多、构建步骤复杂 |
建造者模式(Builder)补充:
建造者模式常与工厂并列讨论,它用于"分步骤构建一个复杂对象"。当一个对象有很多可选参数时,用建造者比用巨长的构造函数参数列表清晰得多。
// 建造者模式:链式构建复杂的查询对象
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);常见坑:
最佳实践:
构造器模式(Constructor):
构造器模式使用构造函数创建对象,初始化对象状态。在 JavaScript 中,构造函数可以与原型链配合实现属性和方法的共享,减少内存消耗。ES6 的 class 语法是构造函数的语法糖,使代码更加清晰。
// 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,就能盖出一栋栋结构相同但内容各异的房子(实例)。
为什么构造器模式重要:
new 操作符到底做了什么(原理):
理解构造器模式必须理解 new 的四个步骤:
// 手写一个 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更多代码变体:
// 变体一:私有字段(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); // 语法错误,无法从外部访问// 变体二:继承与 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// 变体三:用 new.target 防止忘记写 new
function User(name) {
if (!new.target) {
throw new Error('User 必须使用 new 调用');
}
this.name = name;
}
// User('Alice'); // 抛出错误
const u = new User('Alice'); // 正确真实案例:
构造器与相近模式对比:
| 对比项 | 构造器模式 | 工厂模式 | 原型模式 |
| --- | --- | --- | --- |
| 创建方式 | new 构造函数 | 调用工厂方法 | 克隆已有对象 |
| 是否暴露具体类 | 是(要写 new) | 否 | 否 |
| 支持 instanceof | 是 | 取决于实现 | 取决于原型 |
| 灵活度 | 中 | 高 | 高 |
常见坑:
最佳实践:
原型模式(Prototype):
原型模式基于原型链创建对象,通过克隆现有对象来创建新对象。JavaScript 天然支持原型继承,Object.create() 方法是实现原型模式的核心。原型模式适合创建成本较高的对象,通过克隆提高性能。
// 原型模式实现
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() 就是原型模式的语言级支持。
为什么原型模式重要:
深浅拷贝的区别(原理):
原型模式绕不开"拷贝"这个话题。浅拷贝只复制第一层,嵌套对象仍是共享引用;深拷贝则递归复制所有层级,得到完全独立的副本。
// 浅拷贝的几种方式
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,不受影响// 手写一个能处理循环引用的深拷贝
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,循环引用被正确保留// 变体:基于原型对象批量派生配置
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,被覆盖真实案例:
原型与相近模式对比:
| 对比项 | 原型模式 | 构造器模式 | 工厂模式 |
| --- | --- | --- | --- |
| 新对象来源 | 克隆现有对象 | new 构造函数 | 工厂方法返回 |
| 初始化成本 | 低(复用已有状态) | 每次都重新初始化 | 取决于实现 |
| 是否需要类定义 | 不需要 | 需要 | 不一定 |
| JS 语言支持 | 原生(Object.create) | 原生(new) | 需自行封装 |
常见坑:
最佳实践:
结构型模式
结构型模式关注如何把类和对象组合成更大的结构,同时保持结构的灵活和高效。它回答的是"如何用小对象搭建出大系统"的问题。结构型模式大量利用组合优于继承的思想:通过对象组合(把对象作为另一个对象的成员)而非类继承来复用功能,从而获得运行时的灵活性。下面的适配器、桥接、组合、装饰器、外观、享元、代理七个模式各自解决不同的结构问题。
适配器模式(Adapter):
适配器模式转换接口,使不兼容的接口能够一起工作。在 JavaScript 中,适配器模式常用于整合第三方库、处理不同数据格式、统一 API 接口等场景。适配器模式可以在不修改原有代码的情况下,实现接口的兼容。
// 适配器模式示例
// 旧 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);
}
}适配器模式:概念类比与深入理解
适配器最经典的类比就是"电源转换插头":你的笔记本是两脚插头,酒店墙上是三孔插座,两者接口不兼容,于是你用一个转换插头把它们连接起来。转换插头不改变笔记本,也不改变插座,只是在中间做接口转换。适配器模式做的正是这件事——在不修改双方代码的前提下,让接口不兼容的两个对象协同工作。
为什么适配器模式重要:
对象适配器 vs 类适配器(原理):
适配器有两种实现方式。对象适配器通过"持有被适配对象的引用"(组合)来转换接口,这是 JS 中最常用的方式。类适配器通过多重继承实现,但 JS 不支持多重继承,通常用组合替代。
更多代码变体:
// 变体一:适配不同的第三方地图 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 还是百度,业务代码完全一致// 变体二:把 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));// 变体三:数据格式适配——把后端下划线命名转成前端驼峰命名
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 }] }真实案例:
适配器与相近模式对比:
| 对比项 | 适配器 | 外观 | 装饰器 | 代理 |
| --- | --- | --- | --- | --- |
| 意图 | 转换已有接口 | 简化复杂子系统 | 增强功能 | 控制访问 |
| 是否改变接口 | 是(转换成新接口) | 是(提供新的简化接口) | 否(保持原接口) | 否(保持原接口) |
| 是否新增功能 | 否 | 否 | 是 | 可选 |
| 典型场景 | 兼容不兼容接口 | 封装子系统 | 叠加职责 | 缓存/权限/懒加载 |
常见坑:
最佳实践:
装饰器模式(Decorator):
装饰器模式动态为对象添加额外功能,不修改原有代码。在 JavaScript 中,装饰器模式可以通过高阶函数、类装饰器等方式实现。装饰器模式适合在不改变对象结构的情况下,扩展对象的功能。
// 函数装饰器
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 继承(原理):
继承是静态的、编译期确定的,一个对象一旦创建就无法改变其类。装饰器是动态的、运行时的,可以在运行时决定给对象叠加哪些功能、叠加几层、以什么顺序。这就是"组合优于继承"的经典体现。
更多代码变体:
// 变体一:函数装饰器组合——用 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 现在同时具备缓存 + 重试能力// 变体二: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);
}
}// 变体三:属性访问装饰——用 getter/setter 装饰添加校验和日志
function readonly(target, context) {
return {
get() {
return context.access.get.call(this);
},
set() {
throw new Error(`${context.name} 是只读属性`);
},
};
}真实案例:
装饰器与相近模式对比:
| 对比项 | 装饰器 | 代理 | 适配器 | 继承 |
| --- | --- | --- | --- | --- |
| 意图 | 增强功能 | 控制访问 | 转换接口 | 复用+特化 |
| 接口是否改变 | 否 | 否 | 是 | 通常否 |
| 动态/静态 | 动态(运行时组合) | 动态 | 动态 | 静态(编译期) |
| 可叠加多层 | 是 | 通常单层 | 通常单层 | 单继承链 |
常见坑:
最佳实践:
代理模式(Proxy):
代理模式控制对对象的访问,添加额外的行为。ES6 的 Proxy 对象是实现代理模式的强大工具,可以拦截各种操作,如属性访问、赋值、函数调用等。代理模式常用于数据验证、缓存、访问控制等场景。
// 使用 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 调用、统一多个模块的接口、简化库的使用等场景。外观模式可以降低系统的复杂度,提高代码的可读性。
// 外观模式示例
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 都大量使用观察者模式。
// 观察者模式实现
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 中,策略模式常用于表单验证、动画效果、支付方式选择等场景。
// 策略模式实现
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 中,命令模式常用于实现撤销/重做功能、菜单操作、宏命令等场景。
// 命令模式实现
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 实现自定义迭代器。迭代器模式常用于遍历复杂数据结构、实现懒加载等场景。
// 自定义迭代器
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 中,状态模式常用于实现状态机、游戏角色状态、订单状态等场景。
// 状态模式实现
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;
}应用场景
单页应用架构中的设计模式:
// 路由管理器(观察者模式)
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);
}
}表单验证中的设计模式:
// 表单验证器(策略 + 责任链)
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 客户端(代理 + 装饰器)
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];
});动画效果中的设计模式:
// 动画管理器(策略 + 命令)
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 组件
// 抽象产品:按钮
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));代码示例二:多数据库适配
// 用抽象工厂封装不同数据库的连接与查询构造器
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,本质上也是在为不同平台选择不同的产品实现。
与相近模式对比
| 维度 | 工厂方法 | 抽象工厂 |
| --- | --- | --- |
| 创建对象数量 | 一种产品 | 一族相关产品 |
| 关注点 | 单个对象的创建延迟 | 产品之间的一致性 |
| 扩展新产品 | 容易(加一个工厂方法子类) | 困难(要改抽象工厂接口) |
| 扩展新产品族 | 不适用 | 容易(加一个具体工厂) |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 创建一族相互关联对象的接口 |
| 最适用场景 | 跨平台 UI、多主题、多数据库适配 |
| 核心收益 | 保证产品族一致性、切换成本低 |
| 主要代价 | 增加新产品种类困难 |
创建型模式深入:建造者 Builder
概念与类比
建造者模式(Builder)把一个复杂对象的构建过程拆解成一系列步骤,让你可以"按部就班"地组装它。
类比点外卖时的"定制汉堡":你先选面包,再选肉饼,再加芝士、生菜、酱料,最后打包。每一步都是可选的、可组合的,最终得到一个满足你个性化需求的汉堡。制作流程(Builder)和最终产品(汉堡)是分离的,同样的流程可以做出千变万化的汉堡。
为什么重要
当一个对象的构造函数参数超过 4 个,尤其是有很多可选参数时,代码会变得极难阅读和维护。这就是臭名昭著的"望远镜构造函数(telescoping constructor)"反模式:
// 反模式:参数太多,调用时根本看不懂每个值是什么意思
new HttpRequest('https://api.com', 'POST', headers, body, 5000, 3, true, false, null);建造者模式用链式调用 + 语义化方法名彻底解决这个问题,让构建过程自文档化。
原理与结构
核心是让每个设置方法返回 `this`,从而支持链式调用,最后用一个 `build()` 方法产出最终对象。
代码示例一:HTTP 请求建造者
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 查询建造者
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真实案例
与相近模式对比
| 维度 | 建造者 | 工厂方法 | 抽象工厂 |
| --- | --- | --- | --- |
| 构建复杂度 | 分多步构建复杂对象 | 一步创建 | 一步创建一族 |
| 是否关注过程 | 关注构建过程 | 不关注 | 不关注 |
| 典型标志 | 链式调用 + build() | create() | createXxx() 多个 |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 分步构建复杂对象,分离构建与表示 |
| 最适用场景 | 多可选参数对象、查询构造、配置对象 |
| 核心收益 | 消除望远镜构造函数、代码自文档化 |
| 主要代价 | 需要额外的 Builder 类 |
创建型模式深入:原型 Prototype
概念与类比
原型模式(Prototype)通过"克隆"一个已有对象来创建新对象,而不是通过 new 从零构造。
类比细胞分裂:与其从原子开始重新合成一个细胞,不如让现有细胞复制自己。当创建一个对象的成本很高(比如需要复杂计算、数据库查询、网络请求)时,克隆一个已有实例比重新创建快得多。
为什么重要
JavaScript 本身就是基于原型的语言,`Object.create`、原型链、`__proto__` 都是原型思想的体现。理解原型模式,等于理解 JS 语言的底层对象模型。此外,在需要大量相似对象、且初始化成本高的场景(如游戏中的子弹、粒子系统),原型模式能显著提升性能。
代码示例一:基于 Object.create 的原型
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());代码示例二:深浅拷贝的克隆
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) | 是 | 可定制 | 需手动处理 |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 通过克隆已有对象来创建新对象 |
| 最适用场景 | 高成本对象创建、JS 原型继承 |
| 核心收益 | 避免重复初始化开销 |
| 主要代价 | 深浅拷贝语义易混淆 |
结构型模式深入:适配器 Adapter
概念与类比
适配器模式(Adapter)把一个类的接口转换成客户端期望的另一个接口,让原本因接口不兼容而不能一起工作的类能够协同工作。
最经典的类比就是电源转换插头:你的笔记本是两脚插头,墙上是三孔插座,插不进去。买一个转换插头(适配器),一端接你的插头,一端插墙,问题解决。适配器不改变任何一方,只在中间做转换。
为什么重要
前端开发中,适配器无处不在:对接第三方 SDK(微信/支付宝/Google 登录返回的用户数据字段五花八门)、封装后端返回的数据格式、抹平不同浏览器 API 差异、迁移旧系统时兼容老接口。适配器是"防腐层(Anti-Corruption Layer)"的核心实现手段,能把外部系统的混乱挡在业务代码之外。
代码示例一:统一第三方登录数据
// 三个平台返回的用户数据格式完全不同
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
// 旧的日志库,接口老旧
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('发生错误');真实案例
与相近模式对比
| 模式 | 意图 | 何时用 |
| --- | --- | --- |
| 适配器 | 转换已有接口 | 事后兼容不兼容的接口 |
| 装饰器 | 增强功能,接口不变 | 动态添加职责 |
| 外观 | 简化复杂子系统 | 提供统一简单入口 |
| 代理 | 控制访问,接口不变 | 加访问控制/懒加载 |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 转换接口使不兼容的类协同工作 |
| 最适用场景 | 第三方 SDK、遗留系统、数据格式统一 |
| 核心收益 | 隔离外部变化、保护业务代码 |
| 主要代价 | 增加一层间接 |
结构型模式深入:桥接 Bridge
概念与类比
桥接模式(Bridge)将抽象部分与实现部分分离,使它们可以独立变化。
类比遥控器和电视:遥控器是"抽象"(定义了开关、音量、换台等操作),电视是"实现"(具体怎么开机、怎么调音量)。同一个遥控器可以控制不同品牌的电视,同一台电视也可以被不同款式的遥控器控制。两者通过标准化的信号协议(桥)连接,各自独立演进。
为什么重要
桥接解决的是"多维度变化"的组合爆炸问题。假设你有 3 种消息类型(普通、加急、紧急)× 3 种发送渠道(短信、邮件、站内信),如果用继承要写 9 个类;随着维度增加,类的数量呈乘法增长。桥接把两个维度拆开,只需 3 + 3 = 6 个类,用组合替代继承。
代码示例:消息发送系统
// 实现维度:发送渠道
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 的本质。它让"遍历整棵树""对整棵树执行某操作"变得极其简洁。
代码示例:文件系统
// 统一接口:都有 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');代码示例二:菜单树遍历
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)); // ['首页', '个人资料', '隐私']常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 树形结构中统一处理单体与组合 |
| 最适用场景 | 文件系统、组件树、菜单、组织架构 |
| 核心收益 | 递归操作代码极简、客户端无需区分类型 |
| 主要代价 | 类型约束变弱 |
结构型模式深入:外观 Facade
概念与类比
外观模式(Facade)为复杂的子系统提供一个简化的统一入口。
类比餐厅点餐:你只需对服务员说"来一份宫保鸡丁",服务员(外观)会去协调后厨的采购、切配、炒制、摆盘等一系列复杂流程,你不需要知道后厨内部怎么运作。外观把复杂性藏在背后,对外只暴露简单接口。
为什么重要
外观是降低系统耦合度的利器。当多个模块需要协同完成一件事时,与其让调用方了解所有模块并按正确顺序调用,不如提供一个外观方法一键搞定。前端封装的各种 `utils`、`service`、SDK 初始化函数,本质上都是外观。
代码示例:视频转码外观
// 复杂的子系统
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 个读者在读,图书馆不会为每个读者印一本,而是共享同几本,"谁在读、读到第几页"(外部状态)由读者自己记录,"书的内容"(内部状态)大家共享。
为什么重要
在需要创建海量相似对象的场景(如地图上的成千上万个标记点、游戏中的大量子弹/树木、编辑器中每个字符),如果每个对象都独立存储所有数据,内存会爆炸。享元通过共享不变部分,能把内存占用降低一个数量级。
代码示例:地图标记点
// 享元工厂:缓存共享的内部状态(图标类型)
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% |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 共享细粒度对象的不变部分以节省内存 |
| 最适用场景 | 海量相似对象、地图标记、粒子、字符 |
| 核心收益 | 内存占用大幅下降 |
| 主要代价 | 代码复杂度上升、状态划分难 |
行为型模式深入:责任链 Chain of Responsibility
概念与类比
责任链模式(Chain of Responsibility)让多个对象都有机会处理请求,把这些对象连成一条链,请求沿链传递直到有对象处理它为止。
类比公司报销审批:你提交报销单,先到组长,组长权限内的直接批,超权限的转交部门经理,再超转交财务总监,再超转交 CEO。每一级只处理自己权限范围内的请求,处理不了就往上传。请求发起者不需要知道最终是谁批的。
为什么重要
责任链最大的价值是解耦请求发送者与接收者,并让处理逻辑高度可插拔。Express/Koa 的中间件、axios 拦截器、事件冒泡、表单校验链、日志过滤链,本质都是责任链。它让你可以像搭积木一样增删处理环节,符合开闭原则。
代码示例:请求审批链
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); // 没人能批代码示例二:函数式中间件洋葱模型
// 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,返回逆序常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 请求沿处理者链传递直到被处理 |
| 最适用场景 | 中间件、审批流、过滤器、事件冒泡 |
| 核心收益 | 发送者与接收者解耦、处理环节可插拔 |
| 主要代价 | 请求可能无人处理、调试链路难 |
行为型模式深入:命令 Command
概念与类比
命令模式(Command)将请求封装成一个独立对象,从而可以用不同的请求参数化其他对象,并支持请求的排队、记录、撤销。
类比餐厅点菜单:你点的菜被写成一张订单(命令对象),服务员把订单交给后厨。订单把"谁点的、点了什么"封装起来,可以排队、可以取消、可以重做。点菜人和做菜人通过订单解耦,互不直接依赖。
为什么重要
命令模式是实现撤销/重做(undo/redo)、操作日志、宏命令(批量操作)、任务队列的基石。编辑器、绘图软件、事务系统都离不开它。Redux 的 action 本质就是命令对象。
代码示例:可撤销的文本编辑器
// 接收者
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代码示例二:宏命令(批量执行)
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` 都基于它。理解迭代器协议,才能自定义可迭代对象、玩转生成器、处理无限序列和惰性求值。
代码示例:自定义可迭代对象
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]代码示例二:生成器实现惰性无限序列
// 生成器是实现迭代器最简洁的方式
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 组件通信是典型场景。
代码示例:聊天室
// 中介者
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 前存个档,如果失败了就读档回到存档点。存档文件(备忘录)保存了角色的血量、位置、装备等状态,但你看不到也不需要知道存档文件的内部格式。
为什么重要
备忘录是实现快照、撤销、事务回滚、草稿恢复的核心。它和命令模式常常配合使用:命令模式负责"做什么",备忘录负责"状态怎么回退"。表单草稿自动保存、文档历史版本、游戏存档都是它的应用。
代码示例:编辑器状态快照
// 备忘录:只存状态,不含逻辑
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); // 第一句。与命令模式配合的内存对比
| 撤销实现方式 | 内存占用 | 实现复杂度 | 适用 |
| --- | --- | --- | --- |
| 命令模式(存操作) | 低(只存增量) | 中 | 操作可逆、状态小 |
| 备忘录(存快照) | 高(存全量状态) | 低 | 状态难逆推、快照小 |
| 混合(命令+备忘录) | 中 | 高 | 复杂编辑器 |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 保存并恢复对象状态而不破坏封装 |
| 最适用场景 | 撤销、快照、草稿、游戏存档 |
| 核心收益 | 状态可回退、封装不破坏 |
| 主要代价 | 快照占内存 |
行为型模式深入:状态 State
概念与类比
状态模式(State)允许对象在其内部状态改变时改变它的行为,看起来就像修改了它的类。
类比自动门:门有"关闭""打开""正在开""正在关"几种状态。同样是"有人靠近"这个事件,门在不同状态下反应完全不同——关闭时开始打开,正在关时反转。每种状态知道自己该怎么响应事件,以及下一步转到哪个状态。
为什么重要
状态模式用来消灭那种臭名昭著的巨型 `if-else`/`switch` 状态判断。当一个对象的行为强依赖于它的状态、且状态间有明确转换规则时(订单流转、播放器、审批流、红绿灯),状态模式把每个状态的逻辑内聚到独立的状态类里,新增状态不影响其他状态。
代码示例:订单状态机
// 每个状态是一个类,封装该状态下的行为和转换规则
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 | 状态模式 |
| --- | --- | --- |
| 新增状态 | 修改所有判断分支 | 加一个状态类 |
| 状态逻辑内聚 | 分散在各处 | 集中在状态类 |
| 可读性 | 差(分支嵌套) | 好 |
| 代码量 | 少(初期) | 多(类多) |
| 违反开闭原则 | 是 | 否 |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 状态改变时对象行为随之改变 |
| 最适用场景 | 订单、播放器、审批流、红绿灯 |
| 核心收益 | 消灭巨型 if-else、状态逻辑内聚 |
| 主要代价 | 状态类可能爆炸 |
行为型模式深入:模板方法 Template Method
概念与类比
模板方法模式(Template Method)在父类中定义一个算法的骨架,把某些步骤延迟到子类实现,让子类在不改变算法结构的前提下重定义特定步骤。
类比冲泡饮料:泡咖啡和泡茶的流程骨架一样——烧水、冲泡、倒进杯子、加调料。但"冲泡"和"加调料"这两步不同(咖啡用咖啡粉加糖,茶用茶叶加柠檬)。父类定好流程骨架,子类只填不同的步骤。
为什么重要
模板方法是复用算法骨架、消除重复代码的经典手段。当你有多个流程"大同小异"时,把相同的部分提到父类,把不同的部分做成抽象步骤,子类只实现差异部分。React 组件生命周期、各种框架的钩子(hook)方法都是模板方法思想。
代码示例:数据导出流程
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));与策略模式对比
| 维度 | 模板方法 | 策略模式 |
| --- | --- | --- |
| 复用方式 | 继承 | 组合 |
| 变化部分 | 算法的某些步骤 | 整个算法 |
| 骨架控制 | 父类固定骨架 | 无固定骨架 |
| 灵活性 | 编译期确定 | 运行期切换 |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 父类定义算法骨架,子类填充步骤 |
| 最适用场景 | 流程相似、步骤有别的算法族 |
| 核心收益 | 复用骨架、消除重复 |
| 主要代价 | 依赖继承、层级易深 |
行为型模式深入:访问者 Visitor
概念与类比
访问者模式(Visitor)将作用于某对象结构中各元素的操作分离出来,封装成独立的访问者对象,从而可以在不修改元素类的情况下定义作用于这些元素的新操作。
类比大楼里的检查员:同一栋楼(对象结构),今天来消防检查员,明天来卫生检查员,后天来安全检查员。每个检查员(访问者)对每个房间(元素)做不同的检查,但房间本身不需要改变。要新增一种检查,只需派一个新检查员,不用改造大楼。
为什么重要
访问者的杀手级场景是编译器/AST 处理。Babel、ESLint、TypeScript 编译器遍历抽象语法树时,对不同类型的节点(函数声明、变量声明、表达式)做不同处理,且经常要新增处理逻辑(新的 lint 规则、新的转换)——访问者让你把每种处理封装成一个访问者,无需改动 AST 节点类。
代码示例:AST 节点处理
// 元素: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 处理、编译器、报表生成 |
| 核心收益 | 新增操作不改元素类 |
| 主要代价 | 新增元素类型要改所有访问者 |
行为型模式深入:解释器 Interpreter
概念与类比
解释器模式(Interpreter)给定一个语言,定义它的文法表示,并定义一个解释器来解释语言中的句子。
类比翻译官:给定一套语法规则(比如简单的计算表达式),解释器逐条读取、按规则理解并执行。正则引擎、模板引擎、SQL 解析、公式计算器都是解释器的应用。
为什么重要
当你需要处理一种"小语言"(DSL,领域特定语言)——比如自定义的搜索语法、规则引擎表达式、模板占位符——解释器模式提供了系统化的实现思路。它相对小众,但在规则引擎、低代码平台中很关键。
代码示例:布尔表达式解释器
// 上下文:变量的值
// 终结符表达式
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常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 为小语言定义文法并解释执行 |
| 最适用场景 | 规则引擎、简单 DSL、公式计算 |
| 核心收益 | 文法可扩展、结构清晰 |
| 主要代价 | 复杂文法难维护、性能一般 |
JavaScript 特色模式:模块模式与揭示模块模式
概念与类比
模块模式(Module Pattern)利用 JavaScript 的闭包,创建私有作用域,只暴露必要的公共接口,实现真正的封装(在 ES Module 出现之前,这是 JS 实现私有变量的主要手段)。
类比公司的前台:外人(外部代码)只能通过前台(公共接口)办事,公司内部的办公室、财务室(私有变量)外人进不去。
代码示例:经典模块模式
// 立即执行函数(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)是模块模式的变体:所有函数和变量都定义在私有作用域中,最后在返回对象里"揭示"哪些是公开的。它的好处是公开映射一目了然,且公共方法之间调用不用加前缀。
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+ | 现代标准 |
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 用闭包实现私有作用域和封装 |
| 最适用场景 | 老代码、库封装、理解 JS 闭包 |
| 核心收益 | 真正的私有变量 |
| 主要代价 | ESM 时代已非首选 |
JavaScript 特色模式:混入 Mixin
概念与类比
混入(Mixin)把一组方法"掺入"到一个类或对象中,实现横向的功能复用,绕开单继承的限制。
类比给手机装 App:手机(类)本身有基础功能,你想要拍照、导航、支付功能,就装上对应的 App(mixin)。功能是"拼装"上去的,一个手机可以装任意多个 App。
为什么重要
JavaScript 只支持单继承,但现实中一个类可能需要多种不相关的能力(可序列化、可比较、可观察)。混入用组合的方式给类"叠加"能力,比深继承链灵活得多。Vue 2 的 mixin、很多工具库的能力增强都用此模式。
代码示例:对象混入
// 定义可复用的能力片段
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,拥有比较能力代码示例二:类混入(高阶函数式)
// 用返回类的函数实现类混入
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常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 横向复用能力、绕开单继承限制 |
| 最适用场景 | 多能力叠加、跨类复用 |
| 核心收益 | 灵活组合能力 |
| 主要代价 | 命名冲突、来源难追踪 |
JavaScript 特色模式:发布订阅
概念与类比
发布订阅(Publish-Subscribe)通过一个事件中心(Event Bus)解耦消息的发布者和订阅者,二者互不知道对方的存在。
类比报社订阅:读者向报社订阅报纸,报社出新报就派送给所有订阅者。读者不认识写稿的记者,记者也不知道有哪些读者——报社(事件中心)居中调度。
发布订阅 vs 观察者
这两个概念常被混用,但有关键区别:观察者模式中,主题(Subject)直接持有观察者列表并直接通知它们;发布订阅中,发布者和订阅者完全不知道对方,通过第三方事件中心(Broker)通信,耦合更松。
| 维度 | 观察者模式 | 发布订阅 |
| --- | --- | --- |
| 双方是否知道彼此 | 主题知道观察者 | 完全不知道 |
| 是否有中间人 | 无(直接通知) | 有(事件中心) |
| 耦合度 | 较松 | 最松 |
| 典型实现 | Subject.notify() | EventBus.emit() |
代码示例:完整事件中心
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 触发常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 通过事件中心解耦发布者与订阅者 |
| 最适用场景 | 跨组件通信、事件系统、消息广播 |
| 核心收益 | 最大程度解耦 |
| 主要代价 | 内存泄漏风险、数据流难追踪 |
JavaScript 特色模式:依赖注入
概念与类比
依赖注入(Dependency Injection,DI)不在对象内部自己创建依赖,而是从外部把依赖"注入"进来,实现控制反转(IoC)。
类比装修房子:与其让房子自己去生产家具(内部 new),不如你把选好的家具搬进去(注入)。这样想换家具时不用拆房子,只需搬进新家具。
为什么重要
DI 是可测试性和低耦合的关键。当一个类的依赖从外部注入时,测试时就能轻松传入 mock 依赖;替换实现时也无需改动类本身。Angular、NestJS 的核心就是 DI 容器。
代码示例:手动依赖注入
// 不好:内部硬编码依赖,无法替换、难以测试
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 容器
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));最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 从外部注入依赖,实现控制反转 |
| 最适用场景 | 可测试性要求高、大型应用 |
| 核心收益 | 低耦合、易测试、易替换 |
| 主要代价 | 需要容器或手动接线 |
JavaScript 特色模式:柯里化与函数组合
柯里化 Currying
柯里化把一个接受多个参数的函数,转换成一系列每次只接受一个参数的函数。它让函数可以"分步传参",是函数复用和偏应用的利器。
// 通用柯里化工具
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` 从左向右执行(更符合阅读习惯)。
// 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-worldcompose vs pipe 对比
| 维度 | compose | pipe |
| --- | --- | --- |
| 执行方向 | 右 → 左 | 左 → 右 |
| 数学直觉 | 符合 f(g(x)) | 反直觉 |
| 阅读直觉 | 需倒着读 | 顺着读 |
| 代表库 | Redux compose | RxJS pipe、Lodash flow |
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 柯里化分步传参、组合串联小函数 |
| 最适用场景 | 数据管道、函数复用、偏应用 |
| 核心收益 | 高复用、可读性强、易测试 |
| 主要代价 | 过度组合会降低可调试性 |
JavaScript 特色模式:防抖节流与记忆化
防抖与节流作为策略
防抖(debounce)和节流(throttle)本质是控制函数执行频率的两种"策略",常和策略模式配合,根据场景选择。
// 防抖:事件停止 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
记忆化缓存函数计算结果,相同输入直接返回缓存,避免重复计算。是空间换时间的经典优化。
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 次 | 数百万倍 |
记忆化常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 防抖 | 只执行最后一次,适合搜索、resize |
| 节流 | 固定频率执行,适合滚动、拖拽 |
| 记忆化 | 缓存结果,空间换时间 |
| 共同点 | 都用闭包保存状态 |
现代框架中的设计模式:React
React 是设计模式的集大成者。理解 React 的各种"模式",本质就是理解经典设计模式在函数式 UI 里的变体。
高阶组件(HOC)= 装饰器模式
高阶组件接收一个组件,返回一个增强后的新组件,接口保持不变——这正是装饰器模式的定义。
// 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 把"如何渲染"作为一个函数参数传入组件,组件负责逻辑,渲染策略由外部注入——这就是策略模式。
// 组件只管获取数据(逻辑),怎么渲染由 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 自由组合。
// 自定义 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 是命令的执行者。所有状态变更都通过命令描述,可记录、可回放、可时间旅行调试。
// 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` 拦截对象的读写:读取时收集依赖(订阅),写入时通知更新(发布)。这是观察者模式和代理模式的完美结合。
// 极简版 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 变了: 2Redux = 单向数据流 + 中间件责任链
Redux 的中间件机制是责任链模式的典范:每个中间件处理 action 后决定是否传给下一个。
// 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 的洋葱模型尤其经典:请求先由外到内穿过每层中间件的前半段,到达核心后再由内到外穿过后半段。
// 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)
为了不存在或极不可能出现的需求,提前引入复杂抽象。
// 反模式:一个简单加法被过度抽象
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)
一个类/模块承担了过多职责,无所不知、无所不能,牵一发动全身。
// 反模式:一个类什么都干
class AppManager {
login() {}
logout() {}
fetchUsers() {}
renderUI() {}
sendEmail() {}
processPayment() {}
generateReport() {}
connectDatabase() {}
// ...还有 50 个方法
}
// 重构:按职责拆分(单一职责原则)
class AuthService { login() {} logout() {} }
class UserService { fetchUsers() {} }
class PaymentService { processPayment() {} }
class EmailService { sendEmail() {} }单例滥用
把单例当全局变量用,任何地方都能改它的状态,导致隐式耦合和测试困难。
// 反模式:全局单例被到处直接修改
const GlobalState = { user: null, cart: [], theme: 'light', /* 什么都往里塞 */ };
// 任何模块都能改,出了 bug 无从追踪
// 重构:用依赖注入 + 明确的状态管理
// 状态变更走统一入口(如 Redux dispatch),可追踪、可测试单例的问题:全局状态使单元测试互相污染(一个测试改了单例,影响另一个);隐藏依赖关系(看构造函数看不出它依赖了单例);并发环境下有竞态。能用依赖注入就别用单例。
常见反模式速查表
| 反模式 | 症状 | 重构方向 |
| --- | --- | --- |
| 过度设计 | 简单需求套复杂框架 | YAGNI,删抽象 |
| 上帝对象 | 一个类几十个方法 | 单一职责拆分 |
| 单例滥用 | 全局可变状态到处改 | 依赖注入 + 状态管理 |
| 复制粘贴编程 | 大量重复代码 | 提取函数/模板方法 |
| 意大利面代码 | 逻辑纠缠无结构 | 分层 + 模式重构 |
| 魔法数字/字符串 | 硬编码字面量 | 常量/枚举 |
| 回调地狱 | 深层嵌套回调 | Promise/async、责任链 |
| 过早优化 | 没测就优化 | 先测量再优化 |
重构的时机与信号
| 代码坏味道 | 可能适用的模式 |
| --- | --- |
| 大量 if-else 判类型 | 策略、状态、多态 |
| new 散落各处 | 工厂、依赖注入 |
| 重复的算法骨架 | 模板方法 |
| 对象间网状引用 | 中介者 |
| 需要撤销/重做 | 命令 + 备忘录 |
| 第三方接口不匹配 | 适配器 |
| 需要事后增强功能 | 装饰器 |
| 树形结构处理 | 组合、访问者 |
重构原则
反模式小结
| 要点 | 说明 |
| --- | --- |
| 核心教训 | 用错模式比不用更糟 |
| 黄金原则 | YAGNI + KISS + 单一职责 |
| 重构方向 | 从坏味道出发,用模式消除 |
| 前提条件 | 充分的测试覆盖 |
综合对比:23 种 GoF 模式速查
| 模式 | 类型 | 一句话意图 | 前端典型场景 |
| --- | --- | --- | --- |
| 单例 | 创建型 | 全局唯一实例 | 全局配置、缓存、store |
| 工厂方法 | 创建型 | 延迟对象创建到子类 | 创建不同类型组件 |
| 抽象工厂 | 创建型 | 创建产品族 | 跨平台 UI、多主题 |
| 建造者 | 创建型 | 分步构建复杂对象 | 查询构造、请求配置 |
| 原型 | 创建型 | 克隆已有对象 | 深浅拷贝、对象复制 |
| 适配器 | 结构型 | 转换不兼容接口 | 第三方 SDK、数据格式 |
| 桥接 | 结构型 | 分离抽象与实现 | 多维度变化 |
| 组合 | 结构型 | 树形统一处理 | DOM、组件树、菜单 |
| 装饰器 | 结构型 | 动态增强功能 | HOC、中间件增强 |
| 外观 | 结构型 | 简化子系统入口 | SDK 封装、utils |
| 享元 | 结构型 | 共享细粒度对象 | 地图标记、粒子 |
| 代理 | 结构型 | 控制对象访问 | 懒加载、Vue 响应式 |
| 责任链 | 行为型 | 请求沿链传递 | 中间件、审批流 |
| 命令 | 行为型 | 请求封装成对象 | undo/redo、action |
| 解释器 | 行为型 | 解释小语言 | 规则引擎、DSL |
| 迭代器 | 行为型 | 统一遍历集合 | for...of、生成器 |
| 中介者 | 行为型 | 中心协调交互 | 组件联动、聊天室 |
| 备忘录 | 行为型 | 保存恢复状态 | 快照、草稿、存档 |
| 观察者 | 行为型 | 一对多通知 | 事件系统、响应式 |
| 状态 | 行为型 | 状态改变行为 | 订单、播放器 |
| 策略 | 行为型 | 算法可替换 | 表单校验、支付方式 |
| 模板方法 | 行为型 | 定义算法骨架 | 生命周期、流程复用 |
| 访问者 | 行为型 | 操作外置于结构 | AST、编译器 |
学习设计模式的路线建议
学习阶段小结
| 阶段 | 目标 | 关键动作 |
| --- | --- | --- |
| 识别 | 看懂模式 | 读框架源码 |
| 应用 | 会用模式 | 重构真实代码 |
| 融会 | 理解本质 | 掌握 SOLID |
| 克制 | 知道不用 | 保持简单 |
结构型模式深入:代理 Proxy
概念与类比
代理模式(Proxy)为另一个对象提供一个替身或占位符,以控制对这个对象的访问。
类比明星经纪人:你想找明星拍广告,不能直接联系明星,得先过经纪人这一关。经纪人(代理)帮明星(真实对象)筛选请求、谈价格、挡掉骚扰,只有合适的请求才转达给明星。代理和真实对象接口一致,你甚至感觉不到中间隔了一层。
为什么重要
JavaScript 的 `Proxy` 是语言级的代理支持,是 Vue 3 响应式、MobX、各种数据校验和拦截库的底层基础。代理模式常用于:懒加载(虚拟代理)、访问控制(保护代理)、缓存(缓存代理)、日志埋点、远程调用等横切关注点。
代理的分类
| 类型 | 用途 | 例子 |
| --- | --- | --- |
| 虚拟代理 | 延迟创建昂贵对象 | 图片懒加载 |
| 保护代理 | 控制访问权限 | 权限校验 |
| 缓存代理 | 缓存结果 | 记忆化 |
| 远程代理 | 代理远程对象 | RPC、API 客户端 |
| 日志代理 | 记录访问 | 埋点、审计 |
代码示例一:虚拟代理实现图片懒加载
// 真实对象:加载真实图片(昂贵操作)
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 实现数据校验
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);
}代码示例三:缓存代理
// 用 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 适配器
| 维度 | 代理 | 装饰器 | 适配器 |
| --- | --- | --- | --- |
| 目的 | 控制访问 | 增强功能 | 转换接口 |
| 接口变化 | 不变 | 不变 | 改变 |
| 是否知道真实对象 | 是(常自己创建) | 是(外部传入) | 是 |
| 典型 | 懒加载、权限 | 叠加职责 | 兼容接口 |
常见坑
最佳实践
小结
| 要点 | 说明 |
| --- | --- |
| 一句话定义 | 为对象提供替身以控制访问 |
| 最适用场景 | 懒加载、权限、缓存、响应式 |
| 核心收益 | 无侵入地插入横切逻辑 |
| 主要代价 | 性能开销、部分场景受限 |
综合实战:一个模式组合的真实案例
真实项目中很少单独使用一个模式,而是多个模式协同。下面用一个"可撤销的绘图应用"串联多个模式。
// 单例:全局绘图上下文
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 刷新 |
最佳实践
设计模式使用原则:
// 过度设计的例子
class SingletonFactoryBuilder {
// 不必要的复杂性
}
// 简洁的实现
const singleton = {
instance: null,
getInstance() {
if (!this.instance) {
this.instance = { data: {} };
}
return this.instance;
},
};代码组织与可维护性:
// 模块化组织设计模式
// 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';性能考虑:
// 避免内存泄漏
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;
}
}测试设计模式:
// 测试观察者模式
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();
});
});