🎯 一、单例模式:全局状态管理的痛
踩坑经历
在做后台管理系统时,我需要全局共享用户信息。当时我这样写:
javascript
// ❌ 这是三年前的我写的代码
class UserManager {
constructor() {
if (UserManager.instance) {
return UserManager.instance;
}
this.userInfo = null;
UserManager.instance = this;
}
login(user) { this.userInfo = user; }
logout() { this.userInfo = null; }
getUser() { return this.userInfo; }
}
问题很快来了:
- 测试困难 - 每个测试用例都要手动重置 instance
- 服务端渲染 - Node 环境下全局状态会污染其他请求
- TypeScript 类型 - instance 属性类型推导复杂
现在的解法
javascript
// ✅ 用 ES6 Module 天然单例
// store/user.js
let userInfo = null;
export function login(user) {
userInfo = user;
localStorage.setItem('user', JSON.stringify(user));
}
export function logout() {
userInfo = null;
localStorage.removeItem('user');
}
export function getUser() {
if (!userInfo) {
const cached = localStorage.getItem('user');
userInfo = cached ? JSON.parse(cached) : null;
}
return userInfo;
}
// 任何地方导入的都是同一个模块实例
import { getUser } from '@/store/user';
优势:
- 无需手动实现单例逻辑
- 测试时直接 mock 模块
- SSR 友好,每个请求独立作用域
什么时候该用单例?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 全局配置 | ES6 Module | 简单可靠 |
| 状态管理 (小型项目) | Module + 发布订阅 | 轻量 |
| 状态管理 (大型项目) | Vuex/Pinia/Redux | 生态完善 |
| 数据库连接 | 传统单例类 | 需要控制生命周期 |
我的原则:能用 Module 就不用类,能上状态管理库就別自己造轮子。
🔥 二、观察者模式:事件系统的核心
真实场景
电商项目中,用户下单后要触发一系列操作:
- 扣减库存
- 发送短信
- 记录日志
- 推送大数据
最初我用回调嵌套:
javascript
// ❌ 回调地狱
orderService.create(order, () => {
inventoryService.reduce(order.items, () => {
smsService.send(order.phone, () => {
logService.record(order, () => {
// ... 还要继续嵌套
});
});
});
});
用观察者模式重构
javascript
// ✅ 事件驱动
// events/order.js
export const orderEvents = new EventTarget();
// 下单
orderEvents.dispatchEvent(new CustomEvent('order:created', {
detail: { orderId: '12345', items: [...] }
}));
// 各模块独立监听
// inventory.js
orderEvents.addEventListener('order:created', (e) => {
reduceInventory(e.detail.items);
});
// sms.js
orderEvents.addEventListener('order:created', (e) => {
sendSMS(e.detail.phone);
});
// log.js
orderEvents.addEventListener('order:created', (e) => {
recordLog(e.detail);
});
好处:
- 新增业务无需修改下单代码(开闭原则)
- 各模块解耦,单独测试
- 异步处理更灵活
但要注意
- 事件命名规范 - 我们用
模块:动作格式,如order:created - 内存泄漏 - 组件销毁时要 removeEventListener
- 调试困难 - 最好加事件日志中间件
javascript
// 开发环境记录事件
if (process.env.NODE_ENV === 'development') {
const originalDispatch = orderEvents.dispatchEvent;
orderEvents.dispatchEvent = function(event) {
console.log('[Event]', event.type, event.detail);
return originalDispatch.call(this, event);
};
}
🎨 三、策略模式:告别 if-else 的优雅方案
业务场景
支付系统要支持多种支付方式:微信、支付宝、银联、信用卡。
javascript
// ❌ 最初的代码
function pay(method, amount) {
if (method === 'wechat') {
// 微信支付逻辑 50 行
} else if (method === 'alipay') {
// 支付宝逻辑 60 行
} else if (method === 'unionpay') {
// 银联逻辑 70 行
} else if (method === 'credit') {
// 信用卡逻辑 80 行
} else {
throw new Error('不支持的支付方式');
}
}
每次新增支付方式都要改这个函数,还要重新测试所有分支。
策略模式重构
javascript
// ✅ 策略模式
// strategies/payment.js
class PaymentStrategy {
pay(amount) { throw new Error('必须实现 pay 方法'); }
}
class WechatStrategy extends PaymentStrategy {
pay(amount) {
// 微信支付具体逻辑
console.log(`微信支付 ${amount} 元`);
}
}
class AlipayStrategy extends PaymentStrategy {
pay(amount) {
// 支付宝具体逻辑
console.log(`支付宝支付 ${amount} 元`);
}
}
// 策略工厂
const strategies = {
wechat: new WechatStrategy(),
alipay: new AlipayStrategy(),
// 新增支付方式只需在这里添加
};
// 使用
function pay(method, amount) {
const strategy = strategies[method];
if (!strategy) {
throw new Error(`不支持的支付方式:${method}`);
}
strategy.pay(amount);
}
实际收益:
- 新增支付方式只需添加新类,不改动现有代码
- 每个策略独立测试
- 代码审查更聚焦
更轻量的函数式写法
javascript
// ✅ 用对象 + 函数更简洁
const paymentStrategies = {
wechat: (amount) => {
// 微信支付逻辑
},
alipay: (amount) => {
// 支付宝逻辑
},
unionpay: (amount) => {
// 银联逻辑
},
};
function pay(method, amount) {
const strategy = paymentStrategies[method];
if (!strategy) {
throw new Error(`不支持的支付方式:${method}`);
}
strategy(amount);
}
我的建议:JavaScript 中优先用函数式策略,复杂场景再用类。
🛡️ 四、代理模式:Vue 3 响应式的秘密
实际应用场景
API 请求缓存是常见需求。最初我直接在业务代码里写缓存:
javascript
// ❌ 缓存逻辑散落在各处
let userCache = null;
async function getUser(id) {
if (userCache) return userCache;
const res = await fetch(`/api/user/${id}`);
userCache = await res.json();
return userCache;
}
问题:
- 每个接口都要重复写缓存逻辑
- 缓存过期时间不统一
- 难以统一清理缓存
用 Proxy 统一处理
javascript
// ✅ 代理模式实现 API 缓存
function createCachedAPI(baseURL) {
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
return new Proxy({}, {
get(target, method) {
return async function(...args) {
const cacheKey = `${method}:${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
console.log(`[Cache Hit] ${cacheKey}`);
return cached.data;
}
console.log(`[Cache Miss] ${cacheKey}`);
const res = await fetch(`${baseURL}/${method}`, {
method: 'POST',
body: JSON.stringify(args)
});
const data = await res.json();
cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
};
}
});
}
// 使用
const api = createCachedAPI('/api');
const user = await api.getUser(123); // 第一次请求
const user2 = await api.getUser(123); // 命中缓存
实际收益:
- 业务代码无需关心缓存
- 统一控制缓存策略
- 轻松添加日志、重试、错误处理
Vue 3 响应式原理
javascript
// Vue 3 响应式简化版
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key); // 收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return result;
}
});
}
理解 Proxy,就理解了 Vue 3 响应式的核心。
⚠️ 五、这些模式,我劝你慎用
1. 装饰器模式
TypeScript 装饰器语法很诱人,但:
- 需要开启 experimentalDecorators
- 调试困难
- 团队新人学习成本高
替代方案:高阶函数、组合函数
javascript
// ❌ TypeScript 装饰器
function log(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name}`);
return original.apply(this, args);
};
return descriptor;
}
// ✅ 高阶函数更清晰
function withLog(fn, name) {
return function(...args) {
console.log(`Calling ${name}`);
return fn(...args);
};
}
const loggedFn = withLog(originalFn, 'originalFn');
2. 抽象工厂模式
JavaScript 是动态语言,不需要像 Java 那样复杂的抽象工厂。
更好的选择:依赖注入 + 工厂函数
javascript
// ❌ 过度设计
class AbstractFactory { ... }
class ConcreteFactory extends AbstractFactory { ... }
// ✅ 简单直接
function createService(deps) {
return {
doSomething: () => { /* 使用 deps */ }
};
}
📊 六、我的设计模式决策树
markdown
需要全局唯一实例?
├─ 是 → ES6 Module(90% 场景)
└─ 否 → 继续
│
需要解耦事件生产者和消费者?
├─ 是 → 观察者模式 / EventTarget
└─ 否 → 继续
│
有多个可互换的算法/策略?
├─ 是 → 策略模式(函数式优先)
└─ 否 → 继续
│
需要控制对象访问/添加横切逻辑?
├─ 是 → 代理模式
└─ 否 → 继续
│
→ 可能不需要设计模式,直接写逻辑
🎯 七、给新人的建议
1. 先写代码,再想模式
模式是总结 出来的,不是预设的。先把功能实现,发现代码重复、难以维护时,再考虑用模式重构。
2. 理解比记忆重要
不要死记 23 种 GoF 模式的定义。理解每种模式解决什么问题,比记住 UML 图更重要。
3. 警惕"模式自豪感"
我见过太多人(包括三年前的我)为了用模式而用模式。好的代码是简单的代码,不是"优雅"的代码。
4. 学习路径建议
markdown
第一阶段:理解问题
- 观察者模式(事件系统)
- 策略模式(条件分支)
第二阶段:理解框架
- 代理模式(Vue 响应式)
- 单例模式(Module 系统)
第三阶段:理解设计
- 装饰器模式(HOC、中间件)
- 工厂模式(对象创建)
💬 最后
设计模式不是银弹,而是工具箱里的工具。会用工具是能力,知道什么时候不用工具是智慧。
希望我的踩坑经验,能让你少走一些弯路。
🔗 参考资料
- patterns.dev - 现代 Web 设计模式
- 《Learning JavaScript Design Patterns》- Addy Osmani
- Vue 3 源码 - 理解 Proxy 响应式
- 个人项目实战经验总结
原创声明: 本文所有代码示例均来自实际项目经验,非翻译或搬运。欢迎转载,请注明出处。