常见设计模式在 JS 里的轻量用法:单例、发布订阅、策略

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。


一、开篇:为什么要学设计模式?

你可能会想:我写业务能跑就行,为什么要管设计模式?

实际工作中常见这些情况:

  • 权限控制:路由守卫、按钮权限、接口权限各处都在写,逻辑重复、难维护。
  • 消息通知:多个模块要弹 Toast,互相耦合,改一处影响一片。
  • 表单校验:不同字段用不同规则,全写在一个大 if-else 里,难扩展。

这些问题都可以用少量设计模式来简化。下面用单例、发布订阅、策略 三种模式,搭配权限、通知、表单校验三个场景,讲清楚:日常怎么写、为什么这么写、容易踩哪些坑


二、概念扫盲

先把三个模式用一句话说清楚:

模式 一句话 适合场景
单例 全局只存在一个实例,多次调用拿到同一个对象 全局唯一的东西:权限管理器、通知中心、全局配置
发布订阅 发布者发事件,订阅者监听,彼此解耦 一对多、多对多通知:消息通知、事件总线
策略 把多种"算法/规则"封装成可替换的策略 同种操作多种规则:表单校验、支付方式选择

下面按「概念 → 代码 → 实战」来展开。


三、单例模式:权限控制里的"唯一管家"

3.1 是什么?

单例保证:不管你怎么 new、怎么调用,拿到的一定是同一个实例。

3.2 最简单的实现

javascript 复制代码
// 懒汉式单例:用到的时候才创建
function createPermissionManager() {
  let instance = null;

  return function () {
    if (!instance) {
      instance = {
        role: 'guest',
        permissions: [],
        check(perm) {
          return this.permissions.includes(perm);
        },
        setRole(role, perms) {
          this.role = role;
          this.permissions = perms;
        }
      };
    }
    return instance;
  };
}

const getPermissionManager = createPermissionManager();
const pm1 = getPermissionManager();
const pm2 = getPermissionManager();
console.log(pm1 === pm2);  // true

3.3 用 class 实现(更贴近日常写法)

javascript 复制代码
class PermissionManager {
  static instance = null;

  constructor() {
    if (PermissionManager.instance) {
      return PermissionManager.instance;
    }
    this.role = 'guest';
    this.permissions = [];
    PermissionManager.instance = this;
  }

  check(perm) {
    return this.permissions.includes(perm);
  }

  setRole(role, perms) {
    this.role = role;
    this.permissions = perms;
  }
}

// 无论怎么 new,都是同一个
const pm1 = new PermissionManager();
const pm2 = new PermissionManager();
console.log(pm1 === pm2);  // true

3.4 实际用法:按钮权限、路由守卫

javascript 复制代码
// permission.js - 全局唯一的权限管理器
class PermissionManager {
  static instance = null;
  constructor() {
    if (PermissionManager.instance) return PermissionManager.instance;
    this.permissions = [];
    PermissionManager.instance = this;
  }

  init(perms) {
    this.permissions = perms;
  }

  has(perm) {
    return this.permissions.includes(perm);
  }

  // 用于 v-if 指令:<button v-if="permission.has('user:delete')">
  hasPermission(perm) {
    return () => this.has(perm);
  }
}

export const permission = new PermissionManager();

// 在路由守卫里用
// router.beforeEach((to, from, next) => {
//   if (to.meta.perm && !permission.has(to.meta.perm)) {
//     next('/403');
//     return;
//   }
//   next();
// });

要点:路由、按钮、接口都用同一个 permission 实例,权限数据统一维护,避免到处复制逻辑。

3.5 单例的坑

原因 建议
测试时状态残留 单例在测试间共享 提供 reset() 或在测试前 permission.permissions = []
滥用单例 不是全局唯一的东西也做成单例 只对真正"全局唯一"的用单例
忘了初始化 直接用 check 但没 init 在登录成功后统一 permission.init(perms)

四、发布订阅模式:消息通知解耦

4.1 是什么?

发布者发事件,订阅者订阅事件,彼此不直接依赖。发布者不关心谁在监听,订阅者不关心谁在发。

4.2 核心:EventBus

javascript 复制代码
class EventBus {
  constructor() {
    this.events = {};  // { eventName: [fn1, fn2, ...] }
  }

  on(eventName, fn) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(fn);
  }

  off(eventName, fn) {
    if (!this.events[eventName]) return;
    this.events[eventName] = this.events[eventName].filter(cb => cb !== fn);
  }

  emit(eventName, ...args) {
    if (!this.events[eventName]) return;
    this.events[eventName].forEach(fn => fn(...args));
  }
}

4.3 实战:消息通知中心

javascript 复制代码
// 通知中心:单例 + 发布订阅
class NotificationCenter {
  static instance = null;

  constructor() {
    if (NotificationCenter.instance) return NotificationCenter.instance;
    this.events = {};
    NotificationCenter.instance = this;
  }

  // 订阅某种类型的通知
  on(type, handler) {
    if (!this.events[type]) this.events[type] = [];
    this.events[type].push(handler);
  }

  off(type, handler) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter(h => h !== handler);
  }

  // 发布:触发所有订阅者
  emit(type, payload) {
    if (!this.events[type]) return;
    this.events[type].forEach(handler => handler(payload));
  }
}

const notify = new NotificationCenter();

// 业务 A:订单成功
notify.on('orderSuccess', (orderId) => {
  Toast.success(`订单 ${orderId} 创建成功`);
  // 可能还要更新购物车、统计等
});

// 业务 B:支付成功
notify.on('paymentSuccess', (data) => {
  Toast.success('支付成功');
  router.push('/orders');
});

// 某处触发
notify.emit('orderSuccess', 'ORD123');

4.4 完整示例:登录后多处联动

javascript 复制代码
// 用户登录成功后,多个模块要同时反应
notify.on('loginSuccess', (user) => {
  // 模块 1:更新 header 头像
  header.updateAvatar(user.avatar);
});
notify.on('loginSuccess', (user) => {
  // 模块 2:拉取用户权限
  permission.init(user.permissions);
});
notify.on('loginSuccess', () => {
  // 模块 3:刷新待办数量
  todoBadge.refresh();
});

// 登录接口成功后,只发一次
loginApi().then(user => {
  notify.emit('loginSuccess', user);
});

发布者只管 emit,订阅者各自处理,互不依赖,修改一处不影响其他模块。

4.5 发布订阅的坑

原因 建议
内存泄漏 组件销毁后没 off beforeUnmount 里统一 off
事件名魔法字符串 到处写 'orderSuccess' 易 typo 抽成常量 EVENTS.ORDER_SUCCESS
过度解耦 简单父子通信也用 EventBus 能用 props/emit 就用,只在跨层、多对多时用
回调地狱 用事件代替 Promise 异步流程优先用 async/await,事件只做"通知"

五、策略模式:表单校验规则可插拔

5.1 是什么?

把不同校验规则封装成独立策略,用配置或映射表选择执行哪个,避免大段 if-else

5.2 没策略时:容易变成"面条码"

javascript 复制代码
// 反面教材:每加一个规则就要改这里
function validate(value, rule) {
  if (rule === 'required') {
    return value !== '' && value != null;
  }
  if (rule === 'email') {
    return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value);
  }
  if (rule === 'phone') {
    return /^1\d{10}$/.test(value);
  }
  if (rule === 'minLength') {
    return value.length >= 6;
  }
  // 越加越多...
}

5.3 用策略重构

javascript 复制代码
// 策略对象:每个规则是独立函数
const strategies = {
  required(value) {
    return value !== '' && value != null && String(value).trim() !== '';
  },
  email(value) {
    return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value);
  },
  phone(value) {
    return /^1\d{10}$/.test(value);
  },
  minLength(value, min) {
    return (value || '').length >= min;
  },
  maxLength(value, max) {
    return (value || '').length <= max;
  }
};

// 校验器:根据规则名调用对应策略
function validate(value, ruleName, ...ruleArgs) {
  const fn = strategies[ruleName];
  if (!fn) return true;  // 未知规则默认通过
  return fn(value, ...ruleArgs);
}

// 使用
validate('', 'required');           // false
validate('a@b.com', 'email');       // true
validate('12345', 'minLength', 6);  // false

5.4 实战:表单校验配置化

javascript 复制代码
// 表单校验配置
const formRules = {
  username: [
    { strategy: 'required', message: '用户名不能为空' },
    { strategy: 'minLength', params: [3], message: '至少 3 个字符' }
  ],
  email: [
    { strategy: 'required', message: '邮箱不能为空' },
    { strategy: 'email', message: '邮箱格式不正确' }
  ],
  phone: [
    { strategy: 'phone', message: '手机号格式不正确' }
  ]
};

function validateForm(formData) {
  const errors = {};
  for (const [field, rules] of Object.entries(formRules)) {
    for (const rule of rules) {
      const { strategy, params = [], message } = rule;
      const value = formData[field];
      const valid = validate(value, strategy, ...params);
      if (!valid) {
        errors[field] = message;
        break;  // 一个字段只保留第一个错误
      }
    }
  }
  return { valid: Object.keys(errors).length === 0, errors };
}

// 使用
const result = validateForm({
  username: 'ab',
  email: 'invalid',
  phone: '13800138000'
});
console.log(result);
// { valid: false, errors: { username: '至少 3 个字符', email: '邮箱格式不正确' } }

新增字段或规则时,只改配置,不改 validate 核心逻辑。

5.5 策略的坑

原因 建议
策略与业务混在一起 策略里写请求、跳转 策略只做"规则判断",返回 boolean
规则参数传错 minLength 要数字,传了字符串 做参数校验或封装成 createMinLength(6)
和单例搞混 策略不需要全局唯一 策略是无状态的纯函数,不共享实例

六、三者怎么选?

场景 推荐模式 理由
全局唯一:权限、配置、通知中心 单例 避免多处实例、状态不一致
一对多/多对多:登录联动、订单状态 发布订阅 解耦,扩展新监听者不改原有逻辑
多种规则:表单校验、支付方式、折扣计算 策略 规则可插拔,易维护和扩展

可以组合用,例如:单例的通知中心 + 发布订阅 ,或 策略模式 + 单例的校验器


七、小结

模式 一句话 典型用法
单例 全局只一个实例 权限管理器、通知中心、全局配置
发布订阅 发布事件、订阅处理,彼此解耦 消息通知、登录后联动、跨模块通信
策略 多种规则封装成可替换策略 表单校验、支付方式、折扣规则

记住三点:

  1. 单例只给"真正全局唯一"的东西用,并注意测试时重置。
  2. 发布订阅适合跨模块通知,简单通信优先用 props/emit,用完记得 off
  3. 策略模式把规则抽成独立函数,用配置驱动,方便扩展。

设计模式不是炫技,而是让代码更好改、更好测、更少 bug 的工具。先能用、再好用,逐步引入即可。


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

相关推荐
努力学算法的蒟蒻1 小时前
day95(2.24)——leetcode面试经典150
算法·leetcode·面试
悠闲蜗牛�1 小时前
零成本自建前端性能监控平台:从数据采集到可视化告警实战
前端
小米4961 小时前
Js设计模式---策略模式
设计模式·策略模式
二十画~书生1 小时前
【2025年全国大学生电子设计大赛-国二】超声信标定位系统 (J 题)
开发语言·javascript·经验分享·ecmascript·硬件工程
广州华水科技1 小时前
2026年大坝单北斗GNSS形变监测系统推荐榜单
前端
Mike_jia2 小时前
RootDB:开源免费的Web报表工具,让数据可视化如此简单
前端
LawrenceLan2 小时前
31.Flutter 零基础入门(三十一):Stack 与 Positioned —— 悬浮、角标与覆盖布局
开发语言·前端·flutter·dart
前端 贾公子2 小时前
vue3 组件库的设计和实现原理 (下)
前端·javascript·vue.js
你怎么知道我是队长2 小时前
前端学习---HTML---文本标签
前端·学习·html