你是不是见过这样的代码:一个文件几千行,一个函数做了十件事,改一个地方崩三个地方。今天我们不背理论,直接用5种前端最常用的设计模式,把你从“面条代码”里捞出来。学完你会发现:原来代码可以像乐高一样,哪里坏了换哪里。
前言
设计模式不是“高大上”的面试题,而是前辈们踩过无数坑后总结的“套路”。就像下棋有定式,写代码也有常见问题的标准解法。今天我们从实际场景出发,不讲23种全部,只挑前端最常用的5种:单例、观察者、工厂、策略、装饰器。看完你就能立刻用在项目里。
一、单例模式:全局只有一个的“独生子”
场景:全局弹窗、登录框、Store、线程池。你希望整个应用只有一个实例,反复创建会浪费资源或导致状态冲突。
不用的痛:每次调用都new Modal(),结果页面上出现十几个重叠的弹窗。
实现:
class Singleton {
constructor() {
if (!Singleton.instance) {
this.data = [];
Singleton.instance = this;
}
return Singleton.instance;
}
add(item) {
this.data.push(item);
}
}
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true
前端更常见的写法:用闭包或模块(ES6模块本身就是单例)。
// modal.js
let instance;
export function getModal() {
if (!instance) {
instance = new Modal();
}
return instance;
}
现代替代:直接导出对象字面量(export default { show() {} }),ES6模块天然单例。
二、观察者模式:让不相干的组件“悄悄对话”
场景:购物车更新后,导航栏的数字要变、价格要重算、埋点要上报。你不想让购物车直接调用导航栏的方法(耦合太紧)。
不用的痛:购物车里写header.updateCartCount()、sidebar.recalculate()、analytics.track()…每加一个模块,购物车代码就要改一次。
实现:
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);
}
}
}
const bus = new EventBus();
// 购物车模块
bus.emit('cartUpdated', { count: 3 });
// 导航栏模块
bus.on('cartUpdated', (data) => updateCount(data.count));
// 埋点模块
bus.on('cartUpdated', (data) => track('cart', data));
观察者模式是前端最常用的模式之一。Vue的响应式原理、React的事件系统、Node.js的EventEmitter都是它的变体。
三、工厂模式:不用自己 new,让“工厂”替你造
场景:根据不同参数创建不同类型的对象,但创建逻辑复杂(比如需要条件判断、依赖注入)。你不想在业务代码里到处写new和if-else。
不用的痛:每个用到按钮的地方都要写一堆if (type === 'primary') return new PrimaryButton()…重复代码爆炸。
实现:
class ButtonFactory {
createButton(type) {
switch(type) {
case 'primary':
return new PrimaryButton();
case 'danger':
return new DangerButton();
default:
return new DefaultButton();
}
}
}
const factory = new ButtonFactory();
const btn = factory.createButton('primary');
工厂模式把创建对象的逻辑集中管理,业务代码只依赖工厂接口。
更简单的函数工厂:
function createUser(role) {
const base = { createdAt: Date.now() };
if (role === 'admin') {
return { ...base, permissions: ['read', 'write', 'delete'] };
}
return { ...base, permissions: ['read'] };
}
四、策略模式:消灭“if-else 毒瘤”
场景:表单校验:用户名规则、密码规则、邮箱规则各不相同。或者根据用户等级计算折扣:普通会员9折,黄金会员8折,钻石会员7折。你不想写一长串if-else。
不用的痛:一个函数里十几个if-else,加一个新策略要改原有代码,还容易引入bug。
实现:
// 策略对象
const discountStrategies = {
normal: (price) => price * 0.9,
gold: (price) => price * 0.8,
diamond: (price) => price * 0.7,
};
function getDiscount(level, price) {
return discountStrategies[level]?.(price) ?? price;
}
// 使用时
const finalPrice = getDiscount('gold', 100); // 80
校验示例:
const validators = {
required: (val) => val.trim() !== '',
minLength: (val, len) => val.length >= len,
email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
};
function validate(value, rules) {
for (let rule of rules) {
const [name, param] = rule.split(':');
if (!validators[name]?.(value, param)) return false;
}
return true;
}
策略模式把算法(策略)提取成独立对象,可以动态替换、复用。
五、装饰器模式:给代码“贴金”而不改源码
场景:给现有函数添加日志、性能监控、权限校验、缓存功能,但不修改函数本身。
不用的痛:在每个函数内部手动加console.time,加完又删,污染业务逻辑。
实现(JavaScript高阶函数版本,TS装饰器已在之前文章讲过):
function withLog(fn) {
return function(...args) {
console.log(`调用 ${fn.name} 参数:`, args);
const result = fn.apply(this, args);
console.log(`返回值:`, result);
return result;
};
}
function add(a, b) { return a + b; }
const loggedAdd = withLog(add);
loggedAdd(2, 3); // 输出日志,返回5
更通用的装饰器组合:
function withTimer(fn) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`${fn.name} 耗时 ${end - start}ms`);
return result;
};
}
// 组合多个装饰器
const enhanced = withLog(withTimer(add));
装饰器模式让你能“叠加”功能,保持单一职责。
六、实际项目中的组合运用
比如一个用户登录模块:
- 单例模式:全局唯一的
UserStore。 - 工厂模式:根据角色创建不同的用户实例(
createUser('admin'))。 - 观察者模式:登录成功后,触发
userLoggedIn事件,购物车、头像组件、权限菜单分别响应。 - 策略模式:不同等级用户的权限校验策略。
- 装饰器模式:给API请求函数加上缓存、重试、日志。
七、总结:设计模式是“招式”,不是“教条”
- 单例:全局唯一,省资源。
- 观察者:解耦事件发布和订阅。
- 工厂:集中创建对象。
- 策略:消灭if-else,算法可互换。
- 装饰器:动态增强功能。
不要为了用模式而用模式。当你的代码出现重复、难维护、改一处动全身时,想想哪种模式能帮你解耦。写代码就像搭积木,模式是那些标准接口的积木块,让你搭得又快又稳。
如果你觉得今天的“招式”够实用,点个赞让更多人看到。明天我们将聊聊前端架构设计——从技术选型到目录结构,如何搭建一个能支撑三年迭代的项目骨架。我们明天见!