代码写成一锅粥?3个设计模式让你的项目“起死回生”

你的组件里是不是全是if-else?改一个地方,崩三个地方?新来的同事改完你的代码,你看着他,他看你,两人都沉默了。今天我们不背理论,直接用3个前端最常用的设计模式------单例、观察者、策略,把业务从"屎山"变成"积木"。学完你就能拍着胸脯说:"我的代码,谁都敢动。"

前言

设计模式不是"面试八股文",而是前辈们踩过的坑总结成的"套路"。就像做饭有菜谱,写代码也有标准解法。今天我们把场景摆出来:弹窗多次打开、购物车更新通知到处写、表单校验if-else十几层......然后一个个用设计模式把它们治好。

一、单例模式:全局只有一个的"独生子"

场景:你写了个全局弹窗(Modal),用户点按钮就打开。结果用户连续点三次,页面上冒出三个弹窗叠在一起,像俄罗斯方块。

问题代码

js 复制代码
function showModal() {
  const div = document.createElement('div');
  div.className = 'modal';
  div.innerHTML = '我是弹窗';
  document.body.appendChild(div);
}
// 点三次,三个弹窗

单例模式解决

确保无论调用多少次,只创建同一个实例。

js 复制代码
class GlobalModal {
  constructor() {
    if (!GlobalModal.instance) {
      this.element = null;
      GlobalModal.instance = this;
    }
    return GlobalModal.instance;
  }
  show() {
    if (!this.element) {
      this.element = document.createElement('div');
      this.element.className = 'modal';
      this.element.innerHTML = '我是弹窗';
      document.body.appendChild(this.element);
    }
    this.element.style.display = 'block';
  }
  hide() {
    if (this.element) this.element.style.display = 'none';
  }
}
const modal1 = new GlobalModal();
const modal2 = new GlobalModal();
console.log(modal1 === modal2); // true

真实项目更简单的写法:直接导出实例对象。

js 复制代码
// modal.js
export const globalModal = {
  element: null,
  show() { /* ... */ },
  hide() { /* ... */ }
};

应用:全局Store(Pinia/Vuex就是单例)、全局轮询管理器、WebSocket连接。

二、观察者模式:让不相干的组件"悄悄对话"

场景:用户点击"添加购物车",需要同时做三件事:更新购物车角标、弹出"添加成功"提示、发送埋点数据。如果直接在购物车里调用其他模块的方法,代码会变成:

js 复制代码
function addToCart(item) {
  // 添加逻辑...
  header.updateBadge(count);
  toast.show('添加成功');
  analytics.track('add_to_cart', item);
}

每加一个功能,addToCart就要改一次,耦合得像麻花。

观察者模式解决(事件总线)

js 复制代码
// eventBus.js
class EventBus {
  constructor() {
    this.events = {};
  }
  on(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
  }
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(cb => cb(data));
    }
  }
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}
export const bus = new EventBus();
js 复制代码
// 购物车模块
import { bus } from './eventBus';
function addToCart(item) {
  // 添加逻辑...
  bus.emit('cartUpdated', { count: newCount, item });
}
js 复制代码
// 头部模块
import { bus } from './eventBus';
bus.on('cartUpdated', (data) => {
  updateBadge(data.count);
});
js 复制代码
// 埋点模块
bus.on('cartUpdated', (data) => {
  analytics.track('add_to_cart', data.item);
});

现在,要加新功能只管bus.on,不用改购物车代码。Vue的emitter、React的useContext+useReducer其实都用了这个思想。

三、策略模式:消灭if-else毒瘤

场景:用户等级不同,商品折扣不同。你写了一个函数:

js 复制代码
function getDiscount(level, price) {
  if (level === 'normal') return price * 0.95;
  else if (level === 'gold') return price * 0.9;
  else if (level === 'platinum') return price * 0.8;
  else return price;
}

这还好。但当你需要增加"钻石会员"、"企业会员"、"节日特惠"......函数越来越大,改一次心惊胆战。

策略模式解决:把算法抽成独立对象

js 复制代码
const discountStrategies = {
  normal: (price) => price * 0.95,
  gold: (price) => price * 0.9,
  platinum: (price) => price * 0.8,
};
function getDiscount(level, price) {
  const strategy = discountStrategies[level];
  return strategy ? strategy(price) : price;
}

新增会员等级,只需要加一个策略,不用改getDiscount

更复杂的例子:表单验证

js 复制代码
const validators = {
  required: (val) => val && val.trim() !== '',
  minLength: (val, len) => val.length >= len,
  email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
};
function validateField(value, rules) {
  for (let rule of rules) {
    const [name, param] = rule.split(':');
    const validator = validators[name];
    if (validator && !validator(value, param)) {
      return false;
    }
  }
  return true;
}
// 使用
const isValid = validateField('abc@test.com', ['required', 'email']);

以后增加"手机号验证",加一个mobile策略即可,完全符合开闭原则(对扩展开放,对修改封闭)。

四、组合实战:一个购物车结算页面

  • 单例:全局唯一的购物车实例(存储商品列表)。
  • 观察者:商品数量变化时,触发价格重算、优惠券校验、埋点。
  • 策略:根据用户等级计算折扣;根据优惠券类型(满减、打折)计算优惠。
js 复制代码
// 购物车单例
class Cart {
  static instance = null;
  static getInstance() {
    if (!Cart.instance) Cart.instance = new Cart();
    return Cart.instance;
  }
  items = [];
  addItem(item) {
    // 添加逻辑
    bus.emit('cartChanged', this.items);
  }
}
// 价格计算模块监听变化并应用折扣策略
bus.on('cartChanged', (items) => {
  const total = items.reduce((sum, item) => sum + item.price * item.count, 0);
  const discount = discountStrategies[user.level](total);
  renderTotal(discount);
});

各模块独立,改折扣策略不影响购物车;加埋点不影响价格计算。

五、总结:模式是工具,不是教条

  • 单例:保证全局唯一,适合共享资源。
  • 观察者:解耦事件发布和订阅,适合跨组件通信。
  • 策略:消除if-else,算法可互换,适合规则多变场景。

不要为了用模式而用模式。当你的代码出现重复、难维护、改一处动全身时,想想哪种模式能帮你"抽出来"。写代码就像搭积木,模式就是那些标准接口的积木块,让你搭得又快又稳。

相关推荐
不会敲代码11 小时前
从零搭建 AI 日记助手:用 Milvus 向量数据库实现语义搜索
javascript·openai
千寻girling2 小时前
《 Git 详细教程 》
前端·后端·面试
threelab3 小时前
Three.js UV 图像变换效果 | 三维可视化 / AI 提示词
javascript·人工智能·uv
之歆3 小时前
DAY08_CSS浮动与行内块布局实战指南(下)
前端·css
yqcoder3 小时前
CSS Position 全解析:5 种定位模式详解
前端·css
Rhi6374 小时前
从零搭建项目:React 19 + Vite 8 + Tailwind CSS v4 实战配置
前端
竹林8184 小时前
用Viem替代ethers.js:从一次签名失败到完整迁移的实战记录
前端·javascript
之歆4 小时前
DAY08_CSS浮动与行内块布局实战指南(上)
前端·css
light blue bird4 小时前
主子端台二分法任务汇总组件
前端·数据库·.net·桌面端winform