返回博客
技术 2025年3月5日 9 分钟阅读 · 2208 字
JavaScript 设计模式实战:单例、观察者、策略与装饰器
用现代 JavaScript 实现经典设计模式,提升代码可维护性与可扩展性
#JavaScript
#设计模式
#代码架构
本文由 AI 辅助生成,经人工审核发布
设计模式是前人总结的代码组织经验,是解决特定场景下重复问题的模板。本文用现代 JavaScript(ES6+)实现五种经典设计模式,并说明其适用场景与注意事项。
一、单例模式
1.1 意图
保证一个类只有一个实例,并提供全局访问点。适用于全局配置、缓存、日志等需要唯一实例的场景。
1.2 ES6+ 实现
class Singleton {
static #instance = null;
constructor(config) {
if (Singleton.#instance) {
return Singleton.#instance;
}
this.config = config;
this.createdAt = Date.now();
Singleton.#instance = this;
}
static getInstance(config = {}) {
if (!Singleton.#instance) {
Singleton.#instance = new Singleton(config);
}
return Singleton.#instance;
}
getConfig(key) {
return this.config[key];
}
}
const a = new Singleton({ apiUrl: '/api' });
const b = new Singleton({ apiUrl: '/other' });
console.log(a === b); // true
console.log(a.getConfig('apiUrl')); // '/api'(第一次的配置)
1.3 更简洁的模块单例
// config.js —— 利用 ES Module 天然单例特性
const config = {
apiUrl: '/api',
timeout: 5000,
retry: 3
};
export default Object.freeze(config); // 冻结,防止意外修改
1.4 适用场景与陷阱
| 场景 | 说明 |
|---|---|
| 全局配置 | 应用启动时加载,全程只读 |
| 日志服务 | 统一输出入口 |
| 数据库连接 | 连接池复用 |
| 状态管理 | Redux store 本质是单例 |
陷阱:单例引入全局状态,增加耦合度,使单元测试困难。谨慎使用,优先考虑依赖注入。
二、观察者模式与发布订阅模式
2.1 意图
定义对象间一对多的依赖关系,当对象状态变化时,所有依赖者自动收到通知。
2.2 观察者模式实现
class Subject {
#observers = new Set();
subscribe(observer) {
this.#observers.add(observer);
return () => this.#observers.delete(observer); // 返回取消订阅函数
}
notify(data) {
this.#observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} 收到更新:`, data);
}
}
// 使用
const news = new Subject();
const reader1 = new Observer('读者A');
const reader2 = new Observer('读者B');
const unsub1 = news.subscribe(reader1);
news.subscribe(reader2);
news.notify('头条:ES14 新特性发布');
// 读者A 收到更新: 头条:ES14 新特性发布
// 读者B 收到更新: 头条:ES14 新特性发布
unsub1(); // 读者A 取消订阅
news.notify('第二条新闻');
// 仅 读者B 收到
2.3 发布订阅模式
发布订阅与观察者的区别:多了一个”事件中心”作为中间层,发布者和订阅者互不感知。
class EventBus {
#events = new Map();
on(event, callback) {
if (!this.#events.has(event)) {
this.#events.set(event, new Set());
}
this.#events.get(event).add(callback);
return () => this.off(event, callback);
}
off(event, callback) {
this.#events.get(event)?.delete(callback);
}
emit(event, data) {
this.#events.get(event)?.forEach(cb => cb(data));
}
once(event, callback) {
const unsubscribe = this.on(event, (data) => {
callback(data);
unsubscribe();
});
return unsubscribe;
}
}
const bus = new EventBus();
const unsub = bus.on('user:login', (user) => {
console.log('用户登录:', user.name);
});
bus.emit('user:login', { name: 'Alice' });
// 用户登录: Alice
unsub();
bus.emit('user:login', { name: 'Bob' }); // 无输出
2.4 两者对比
| 维度 | 观察者模式 | 发布订阅模式 |
|---|---|---|
| 结构 | Subject → Observer | Publisher → Bus → Subscriber |
| 耦合度 | 较高(直接通知) | 较低(通过事件中心) |
| 适用场景 | 数据双向绑定 | 跨组件通信、模块解耦 |
2.5 实际应用
- Vue 响应式系统:数据变化时通知视图更新
- Node.js EventEmitter:
emitter.on('event', cb)/emitter.emit('event') - 浏览器事件:
addEventListener本质是观察者模式
三、策略模式
3.1 意图
定义一系列算法,将每个算法封装起来,使它们可以互换。策略模式让算法的变化独立于使用算法的客户端。
3.2 实现
// 定义策略集合
const pricingStrategies = {
normal: (price) => price,
member: (price) => price * 0.9, // 会员九折
vip: (price) => price * 0.8, // VIP 八折
fullReduction: (price) => { // 满 200 减 30
return price >= 200 ? price - 30 : price;
},
coupon: (price, coupon) => { // 优惠券
return Math.max(0, price - coupon);
}
};
// 上下文
class PriceCalculator {
#strategy;
constructor(strategy = 'normal') {
this.setStrategy(strategy);
}
setStrategy(name) {
if (!pricingStrategies[name]) {
throw new Error(`未知策略: ${name}`);
}
this.#strategy = pricingStrategies[name];
}
calculate(price, ...args) {
return this.#strategy(price, ...args);
}
}
// 使用
const calc = new PriceCalculator('normal');
console.log(calc.calculate(250)); // 250
calc.setStrategy('member');
console.log(calc.calculate(250)); // 225
calc.setStrategy('fullReduction');
console.log(calc.calculate(250)); // 220
calc.setStrategy('coupon');
console.log(calc.calculate(250, 50)); // 200
3.3 用 Map 简化表驱动
const validators = new Map([
['required', (v) => v !== '' && v != null],
['email', (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)],
['minLength', (v, n) => String(v).length >= n],
['maxValue', (v, max) => Number(v) <= max],
]);
function validate(value, rules) {
return rules.every(({ rule, params = [] }) =>
validators.get(rule)(value, ...params)
);
}
validate('test@example.com', [
{ rule: 'required' },
{ rule: 'email' }
]); // true
3.4 适用场景
- 表单验证规则
- 价格计算/折扣策略
- 排序算法切换
- 权限控制策略
策略模式消除了大量的
if/else,将每个策略独立封装,新增策略时不修改已有代码(开闭原则)。
四、装饰器模式
4.1 意图
动态地给对象添加额外职责,而不改变其接口。装饰器是继承的灵活替代方案。
4.2 函数装饰器实现
// 日志装饰器
function withLog(fn, name = fn.name) {
return function(...args) {
console.log(`[${name}] 调用,参数:`, args);
const result = fn.apply(this, args);
console.log(`[${name}] 返回:`, result);
return result;
};
}
// 性能计时装饰器
function withTiming(fn, name = fn.name) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`[${name}] 耗时: ${(end - start).toFixed(2)}ms`);
return result;
};
}
// 缓存装饰器
function withCache(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('命中缓存');
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 组合装饰器
function compose(...decorators) {
return (fn) => decorators.reduceRight((f, dec) => dec(f), fn);
}
// 使用
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const enhancedFib = compose(
withLog,
withTiming,
withCache
)(fibonacci);
enhancedFib(10);
4.3 类方法装饰器(ES2023)
// 装饰器函数签名
function log(target, context) {
const originalMethod = target;
function replacement(...args) {
console.log(`调用 ${context.name},参数:`, args);
const result = originalMethod.apply(this, args);
console.log(`${context.name} 返回:`, result);
return result;
}
return replacement;
}
class APIClient {
@log
async fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
@log
async createUser(data) {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
});
return res.json();
}
}
4.4 适用场景
- 日志记录、性能监控
- 权限校验
- 缓存
- 重试机制
// 重试装饰器
function withRetry(fn, { times = 3, delay = 1000 } = {}) {
return async function(...args) {
let lastError;
for (let i = 0; i < times; i++) {
try {
return await fn.apply(this, args);
} catch (err) {
lastError = err;
console.log(`第 ${i + 1} 次失败,${delay}ms 后重试`);
await new Promise(r => setTimeout(r, delay));
}
}
throw lastError;
};
}
const fetchWithRetry = withRetry(fetch, { times: 3, delay: 500 });
五、工厂模式与模块模式
5.1 简单工厂
class Button {
constructor({ text, color, size }) {
this.text = text;
this.color = color;
this.size = size;
}
render() {
console.log(`渲染按钮: ${this.text} (${this.color}, ${this.size})`);
}
}
class ButtonFactory {
static create(type) {
const presets = {
primary: { text: '确认', color: 'blue', size: 'medium' },
danger: { text: '删除', color: 'red', size: 'medium' },
ghost: { text: '取消', color: 'gray', size: 'small' },
};
const config = presets[type] ?? presets.primary;
return new Button(config);
}
}
const btn = ButtonFactory.create('danger');
btn.render(); // 渲染按钮: 删除 (red, medium)
5.2 抽象工厂
// 抽象工厂:创建相关产品族
class UIFactory {
createButton() { throw new Error('未实现'); }
createInput() { throw new Error('未实现'); }
}
class MaterialFactory extends UIFactory {
createButton() { return { type: 'Material Button', theme: 'material' }; }
createInput() { return { type: 'Material Input', theme: 'material' }; }
}
class AntDesignFactory extends UIFactory {
createButton() { return { type: 'AntD Button', theme: 'antd' }; }
createInput() { return { type: 'AntD Input', theme: 'antd' }; }
}
function renderUI(factory) {
const button = factory.createButton();
const input = factory.createInput();
console.log(button, input);
}
renderUI(new MaterialFactory());
renderUI(new AntDesignFactory());
5.3 模块模式
ES Module 天然是模块模式的最佳实现:
// user-service.js
const privateState = new WeakMap();
class UserService {
constructor() {
privateState.set(this, { cache: new Map() });
}
async getUser(id) {
const state = privateState.get(this);
if (state.cache.has(id)) return state.cache.get(id);
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
state.cache.set(id, user);
return user;
}
}
export default new UserService(); // 单例导出
export const VERSION = '1.0.0'; // 常量导出
六、模式对比与选择
| 模式 | 解决的问题 | 关键特征 | 常见场景 |
|---|---|---|---|
| 单例 | 全局唯一实例 | 私有构造 + 静态获取 | 配置、连接池 |
| 观察者 | 一对多通知 | Subject 直接通知 | 数据绑定、事件系统 |
| 发布订阅 | 解耦的跨模块通信 | 通过事件中心中转 | 消息总线、组件通信 |
| 策略 | 算法可互换 | 封装策略 + 上下文切换 | 验证、计价、排序 |
| 装饰器 | 动态扩展功能 | 包裹原函数/类 | 日志、缓存、重试 |
| 工厂 | 封装对象创建 | 集中创建逻辑 | UI 组件、复杂对象 |
| 模块 | 封装与访问控制 | 私有 + 公开接口 | 服务层、工具库 |
七、设计原则
设计模式的背后是 SOLID 原则:
| 原则 | 缩写 | 含义 |
|---|---|---|
| 单一职责 | SRP | 一个类/函数只做一件事 |
| 开闭原则 | OCP | 对扩展开放,对修改关闭 |
| 里氏替换 | LSP | 子类能替换父类使用 |
| 接口隔离 | ISP | 不依赖不需要的接口 |
| 依赖倒置 | DIP | 依赖抽象,不依赖具体实现 |
- 策略模式体现了开闭原则——新增策略不改已有代码
- 观察者模式体现了依赖倒置——Subject 依赖 Observer 抽象接口
- 装饰器模式体现了单一职责——每个装饰器只关注一个横切关注点
总结
设计模式不是教条,而是工具。使用时遵循:
- 不过度设计:简单场景用
if/else足够,不要硬套模式 - 理解意图:每个模式解决的问题比实现细节更重要
- 结合语言特性:ES6+ 的 class、Proxy、Module 让许多模式更简洁
- 关注可测试性:好的模式天然提升代码可测试性
- 适时重构:当重复代码出现三次以上,考虑用模式抽象
模式是手段,可维护、可扩展的代码才是目的。在实际项目中灵活运用,而非为了模式而模式。