我在 3 个项目中踩坑后,才真正理解了 JavaScript 设计模式

🎯 一、单例模式:全局状态管理的痛

踩坑经历

在做后台管理系统时,我需要全局共享用户信息。当时我这样写:

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; }
}

问题很快来了:

  1. 测试困难 - 每个测试用例都要手动重置 instance
  2. 服务端渲染 - Node 环境下全局状态会污染其他请求
  3. 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);
});

好处

  • 新增业务无需修改下单代码(开闭原则)
  • 各模块解耦,单独测试
  • 异步处理更灵活

但要注意

  1. 事件命名规范 - 我们用 模块:动作 格式,如 order:created
  2. 内存泄漏 - 组件销毁时要 removeEventListener
  3. 调试困难 - 最好加事件日志中间件
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、中间件)
  - 工厂模式(对象创建)

💬 最后

设计模式不是银弹,而是工具箱里的工具。会用工具是能力,知道什么时候不用工具是智慧

希望我的踩坑经验,能让你少走一些弯路。


🔗 参考资料

  1. patterns.dev - 现代 Web 设计模式
  2. 《Learning JavaScript Design Patterns》- Addy Osmani
  3. Vue 3 源码 - 理解 Proxy 响应式
  4. 个人项目实战经验总结

原创声明: 本文所有代码示例均来自实际项目经验,非翻译或搬运。欢迎转载,请注明出处。


相关推荐
我爱吃土豆11192 小时前
从零到上架:Chrome 新标签页生产力扩展 FocusTab
前端·产品
子淼8122 小时前
Kali Linux 入门指南:基础操作与常用指令解析
前端
Highcharts.js2 小时前
Highcharts时间线图(Timeline Chart)完全指南:事件序列的可视化叙事图表
javascript·信息可视化·数据分析·highcharts·图表开发·时间线图表
QYR市场调研2 小时前
低密度聚乙烯市场竞争格局变化趋势
前端
学以智用2 小时前
Vue 3 组件完全指南
前端·vue.js
重庆穿山甲2 小时前
Java开发者的大模型入门:AgentScope Java组件全攻略(一)
前端·后端
LawrenceLan3 小时前
36.Flutter 零基础入门(三十六):StatefulWidget 与 setState 进阶 —— 动态页面必学
开发语言·前端·flutter·dart
ego.iblacat3 小时前
Web 技术与 Nginx 网站环境部署
运维·前端·nginx
ricky_fan3 小时前
(已解决)安装openclaw龙虾[特殊字符]npm权限问题EACCES
前端·npm·node.js