设计模式
设计模式是经过验证的、可复用的解决方案模板,解决的是反复出现的特定设计问题。JavaScript 语言本身足够灵活(函数一等公民、原型继承、动态对象),很多经典模式可以写得更轻量。本文按三大分类逐一讲解,重点落在可直接抄用的 JS 实现上。
一、概念与分类
| 分类 | 关注点 | 常见模式 |
|---|---|---|
| 创建型 | 对象"怎么创建",解耦创建与使用 | 单例、工厂、抽象工厂、建造者、原型 |
| 结构型 | 对象"怎么组合",组合出更大的结构 | 适配器、装饰器、代理、组合、外观、享元 |
| 行为型 | 对象"怎么协作",分配职责与交互 | 观察者、发布订阅、策略、模板方法、迭代器、状态、职责链 |
判断一个"模式"是否适合用:问题必须真的反复出现。一次性的场景套模式只会增加复杂度。
二、创建型模式
1. 单例模式(Singleton)
保证一个类全局只有一个实例,并提供统一访问入口。JS 中用模块级变量 + 惰性初始化实现:
class Config {
constructor() {
if (Config.instance) return Config.instance; // 已存在则直接返回
this.theme = "light";
Config.instance = this;
}
static getInstance() {
if (!Config.instance) Config.instance = new Config();
return Config.instance;
}
}
const a = new Config();
const b = Config.getInstance();
console.log(a === b); // true,全局唯一更现代的写法:模块导出天然是单例。
// config.js
export const config = { theme: "light", init() { /* ... */ } };2. 工厂模式(Factory)
用一个函数统一创建对象,调用方不需要知道具体类。JS 中工厂就是一个返回对象的函数:
function createUser(type, data) {
switch (type) {
case "admin": return { role: "admin", ...data, permissions: ["*"] };
case "vip": return { role: "vip", ...data, permissions: ["vip-content"] };
default: return { role: "normal", ...data, permissions: [] };
}
}
const u = createUser("admin", { name: "小明" }); // 只关心 type,不关心类| 对比 | 直接 new | 工厂 |
|---|---|---|
| 调用方依赖 | 具体类 | 工厂函数 |
| 新增类型 | 改调用处 | 只改工厂内部 |
| 复杂度 | 低 | 略高 |
3. 抽象工厂(Abstract Factory)
工厂之上再包一层:创建"一族相关对象"。JS 中通常用对象字面量实现,不强制类:
const darkTheme = { createButton: () => ({ cls: "btn-dark" }), createPanel: () => ({ cls: "panel-dark" }) };
const lightTheme = { createButton: () => ({ cls: "btn-light" }), createPanel: () => ({ cls: "panel-light" }) };
function buildUI(theme) { // theme 即抽象工厂
const btn = theme.createButton();
const panel = theme.createPanel();
return { btn, panel };
}
buildUI(darkTheme); // 整套都是暗色4. 建造者模式(Builder)
分步构建复杂对象,让构造过程可读、可复用。JS 中常用链式调用实现:
class OrderBuilder {
constructor() { this.order = { items: [], coupon: null }; }
addItem(item) { this.order.items.push(item); return this; } // 返回 this 支持链式
applyCoupon(code) { this.order.coupon = code; return this; }
build() { return this.order; }
}
const order = new OrderBuilder()
.addItem({ id: 1, price: 99 })
.addItem({ id: 2, price: 20 })
.applyCoupon("OFF10")
.build();5. 原型模式(Prototype)
以已有对象为模板克隆新对象。JS 的原型链天然契合,Object.create 就是官方克隆工具:
const baseUser = { role: "normal", greet() { return `hi ${this.name}`; } };
const u1 = Object.create(baseUser); // 共享 baseUser 上的方法
u1.name = "小明";
const u2 = Object.create(baseUser);
u2.name = "小红";
console.log(u1.greet(), u2.greet()); // hi 小明 / hi 小红,方法只有一份三、结构型模式
1. 适配器模式(Adapter)
让接口不匹配的双方协同工作,不改原对象,只加一层转换。典型场景:旧 API 与新 API 对接。
// 旧接口:回调风格
const legacyApi = { getData(cb) { cb({ code: 0, data: [1, 2, 3] }); } };
// 新接口:Promise 风格
const adapter = {
getData() {
return new Promise(resolve => legacyApi.getData(res => resolve(res.data)));
},
};
await adapter.getData(); // [1, 2, 3],业务代码无需感知旧接口2. 装饰器模式(Decorator)
动态给对象附加职责,不修改原对象。JS 中高阶函数就是天然装饰器:
function withLogging(fn) {
return function (...args) {
console.log("call:", args);
const result = fn(...args);
console.log("result:", result);
return result;
};
}
function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);
loggedAdd(1, 2); // call: [1, 2] / result: 3可以叠加多层装饰(缓存、鉴权、日志、限流),顺序组合。
3. 代理模式(Proxy)
为对象提供替身,控制对原对象的访问。JS 内置 Proxy 是最直接实现,可拦截属性读写、方法调用等 13 种操作:
const cache = new Map();
const proxy = new Proxy(fetch, {
apply(target, thisArg, args) {
const key = args[0];
if (cache.has(key)) return Promise.resolve(cache.get(key)); // 命中缓存不走原函数
return target(...args).then(res => { cache.set(key, res); return res; });
},
});
await proxy("/api/a"); // 首次请求
await proxy("/api/a"); // 命中缓存4. 组合模式(Composite)
树形结构统一处理叶子与容器。文件系统、DOM、菜单树都是典型:
class TreeNode {
constructor(name) { this.name = name; this.children = []; }
add(child) { this.children.push(child); return this; }
render(depth = 0) {
console.log(" ".repeat(depth) + this.name);
this.children.forEach(c => c.render(depth + 1));
}
}
const root = new TreeNode("root").add(
new TreeNode("a").add(new TreeNode("a1")),
new TreeNode("b"),
);
root.render(); // 叶子与容器用同一接口,递归遍历5. 外观模式(Facade)
为复杂子系统提供统一入口。前端"封装请求函数""组件 API"都是外观:
// 复杂子系统:鉴权 + 缓存 + 上报 + 请求
const facade = {
async loadUser(id) {
const cached = cache.get(id);
if (cached) return cached;
const user = await api.get(`/user/${id}`);
cache.set(id, user);
track(`load_user_${id}`);
return user;
},
};
// 调用方只面对一个简单方法
const user = await facade.loadUser(1);6. 享元模式(Flyweight)
共享不可变状态,减少对象数量。大量相同结构的对象(如粒子、表格单元格)适用:
// 共享"样式"这类不可变数据,每个粒子只存自己的位置
const flyweight = new Map();
function getStyle(color, size) {
if (!flyweight.has(color + size)) flyweight.set(color + size, { color, size });
return flyweight.get(color + size);
}
const p1 = { x: 1, y: 2, style: getStyle("red", 3) };
const p2 = { x: 4, y: 5, style: getStyle("red", 3) };
console.log(p1.style === p2.style); // true,样式对象只存在一份四、行为型模式
1. 观察者模式与发布订阅
两者常被混用,区别在于是否经过中间层:
| 对比 | 观察者(Observer) | 发布订阅(Pub/Sub) |
|---|---|---|
| 关系 | 观察者直接订阅主题 | 发布者与订阅者互不感知 |
| 中间层 | 无 | 事件中心 |
| 耦合 | 低 | 更低 |
| 典型应用 | Vue 响应式 | EventEmitter、消息总线 |
// 发布订阅:一个迷你 EventEmitter
class EventEmitter {
#handlers = new Map();
on(event, fn) { // 订阅
if (!this.#handlers.has(event)) this.#handlers.set(event, []);
this.#handlers.get(event).push(fn);
}
emit(event, ...args) { // 发布
(this.#handlers.get(event) || []).forEach(fn => fn(...args));
}
off(event, fn) { // 退订
const list = this.#handlers.get(event) || [];
this.#handlers.set(event, list.filter(f => f !== fn));
}
}
const bus = new EventEmitter();
bus.on("login", user => console.log(`${user.name} 登录`));
bus.emit("login", { name: "小明" });2. 策略模式(Strategy)
封装一组可互换的算法,运行时选择。与"用对象表替代多分支"一脉相承(见重构篇):
const priceStrategies = {
normal: amount => amount,
member: amount => amount * 0.9,
vip: amount => amount * 0.8,
festival: amount => amount * 0.7,
};
function calcPrice(level, amount) {
const fn = priceStrategies[level] || priceStrategies.normal;
return fn(amount);
}
calcPrice("vip", 100); // 803. 模板方法模式(Template Method)
父类定义流程骨架,子类填充可变步骤。JS 中常用"默认实现 + 覆盖":
class DataParser {
parse(raw) {
const cleaned = this.clean(raw); // 固定流程
const rows = this.split(cleaned);
return this.mapToModel(rows); // 可变步骤交给子类
}
clean(raw) { return raw.trim(); } // 默认实现
split(cleaned) { return cleaned.split("\n"); }
mapToModel(rows) { throw new Error("子类实现"); }
}
class CsvParser extends DataParser {
mapToModel(rows) { return rows.map(r => r.split(",")); }
}
new CsvParser().parse("a,b\nc,d"); // [["a","b"],["c","d"]]4. 迭代器模式(Iterator)
统一方式遍历集合,不暴露内部结构。ES6 的 Symbol.iterator 让自定义结构可 for...of:
const range = {
from: 1, to: 5,
[Symbol.iterator]() {
let cur = this.from;
return {
next: () => cur <= this.to
? { value: cur++, done: false }
: { value: undefined, done: true },
};
},
};
console.log([...range]); // [1, 2, 3, 4, 5]5. 状态模式(State)
对象行为随内部状态变化。把每个状态的行为封装成对象,替代大量 if/else:
const states = {
pending: { next: "paid", label: "待支付" },
paid: { next: "shipped", label: "已支付" },
shipped: { next: "done", label: "已发货" },
done: { next: null, label: "已完成" },
};
class Order {
constructor() { this.state = "pending"; }
advance() {
const next = states[this.state].next;
if (!next) throw new Error("已是终态");
this.state = next;
}
}
const o = new Order();
o.advance(); console.log(o.state); // paid6. 职责链模式(Chain of Responsibility)
请求沿链传递,直到有对象处理它。中间件、表单校验链都是典型:
function validateAge(next) {
return data => (data.age >= 18 ? next(data) : { ok: false, msg: "未成年" });
}
function validateEmail(next) {
return data => (/@/.test(data.email) ? next(data) : { ok: false, msg: "邮箱非法" });
}
const chain = validateAge(validateEmail(data => ({ ok: true, data })));
chain({ age: 20, email: "a@b.com" }); // { ok: true, data: {...} }五、模式选择建议
| 场景特征 | 推荐模式 |
|---|---|
| 全局唯一资源(配置、连接池) | 单例 |
| 创建逻辑复杂、调用方不该知道细节 | 工厂/抽象工厂 |
| 需要给对象动态加能力(日志/缓存/鉴权) | 装饰器 |
| 想控制对昂贵对象的访问(懒加载、权限) | 代理 |
| 对象间松耦合通信(事件驱动) | 发布订阅 |
| 运行时切换算法/规则 | 策略 |
| 对象行为随状态切换 | 状态 |
| 多条件校验、可扩展处理链 | 职责链 |
选择原则:先让代码正确、直白,出现真实痛点(大量重复、难以扩展、耦合过深)再引入模式。模式是工具箱,不是装饰品。
六、真实应用举例
| 应用场景 | 背后模式 | 说明 |
|---|---|---|
Node.js EventEmitter、Vue 事件总线 | 发布订阅 | 组件/模块间解耦通信 |
Vue 响应式 dep/watcher | 观察者 | 数据变化自动通知视图更新 |
React 虚拟 DOM 的 diff 打补丁 | 装饰器思想 | 在原生 DOM 操作外套一层批处理优化 |
图片懒加载 new Proxy(Image, ...) | 代理 | 滚动到视口才真正请求资源 |
| axios 拦截器、Koa/Express 中间件 | 职责链 | 请求按顺序经过各层处理 |
| 路由系统(history/hash 两套实现) | 策略 | 同一接口不同底层实现可切换 |
Redux.createStore | 工厂 | 统一创建 store 实例 |
| 表单校验规则集合 | 策略 | 每一条规则独立封装可组合 |
设计模式的价值不在背名字,而在复用已经被验证的结构决策:看到 EventEmitter 就知道是发布订阅,看到拦截器就想到职责链,沟通成本因此大幅降低。带着"这个痛点对应哪个模式"的问题去学习,比记忆定义高效得多。