大家好~ 作为前端开发者,你是否曾遇到这些问题:
-
项目初期代码清爽,后期越改越乱,新增功能要动多处代码?
-
团队协作时,每个人的代码风格差异大,对接成本高?
-
想复用现有代码,却发现耦合度太高,牵一发而动全身?
其实,这些问题大多可以通过 设计模式 来解决。设计模式不是银弹,但它是前辈们总结的“最佳实践”,能帮我们写出更可维护、可扩展、可复用的代码。
今天,我就用 “通俗比喻+超简化代码+可视化图解” 的方式,把前端最常用的9种设计模式(外观、代理、工厂、单例、策略、迭代器、观察者、中介者、访问者)讲明白。全程无晦涩术语,新手也能轻松看懂!
提示:文章末尾有9种模式的优缺点对比表格,建议先收藏,方便后续查阅~
一、先搞懂:9大设计模式的3大分类
很多同学学设计模式会混乱,其实按“核心作用”可以把9大模式分成3类,对应软件设计的3个核心问题,先分清类别再学,思路会清晰很多:
| 分类 | 核心作用(解决什么问题) | 包含的设计模式 | 通俗比喻 |
|---|---|---|---|
| 创建型模式 | 封装对象的创建过程,让对象创建更灵活、更规范,降低创建对象的耦合度 | 工厂模式、单例模式 | 相当于“奶茶店的后厨”:负责按需求创建奶茶(对象),你不用管奶茶怎么制作(创建细节) |
| 结构型模式 | 处理类或对象的组合关系,通过合理的结构组合,让系统更灵活、可扩展 | 外观模式、代理模式 | 相当于“房屋的装修结构”:把不同的建材(类/对象)按合理方式组合,满足不同需求(功能) |
| 行为型模式 | 定义对象间的交互规则和职责分配,让对象间的通信更清晰、更解耦 | 策略模式、迭代器模式、观察者模式、中介者模式、访问者模式 | 相当于“公司的部门协作规则”:明确不同部门(对象)的职责和沟通方式,避免混乱 |
补充说明:不同资料对设计模式的分类可能有细微差异,但核心逻辑一致。这里按前端最常用的分类方式,聚焦“实用场景”而非纯理论,方便大家理解和应用。
二、外观模式(Facade Pattern)(结构型)
1. 定义
为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用。核心是“封装复杂性,提供简洁接口”。
2. 通俗比喻
你去餐厅吃饭,不需要直接找后厨的厨师、配菜员、洗碗工——只需要找服务员点单即可。这里的“服务员”就是外观模式的核心,它封装了“点餐-做菜-上菜-结账”的整个复杂流程,给你提供了“点单”和“结账”两个简单接口。
3. 超简化代码示例(前端场景:封装复杂的DOM操作)
// 子系统:复杂的DOM操作(单独写会很繁琐)
function createDiv(content) {
const div = document.createElement('div');
div.innerText = content;
return div;
}
function setStyle(element, style) {
for (let key in style) {
element.style[key] = style[key];
}
}
function appendToBody(element) {
document.body.appendChild(element);
}
// 外观类:提供统一接口
class DOMFacade {
// 统一接口:创建带样式的元素并添加到body
createAndAppendDiv(content, style) {
const div = createDiv(content);
setStyle(div, style);
appendToBody(div);
}
}
// 调用者:直接使用外观接口,无需关注子系统细节
const domHelper = new DOMFacade();
domHelper.createAndAppendDiv('外观模式示例', { color: 'red', fontSize: '20px' });
4. 图解思路
用流程图展示外观模式的交互逻辑,核心是“调用者只对接外观类,不用管子系统细节”:
解读:调用者想完成“创建带样式的div并添加到页面”,不用分别调用createDiv、setStyle、appendToBody三个方法,只需要调用外观类DOMFacade的createAndAppendDiv接口即可。外观类内部会自动协调三个子系统方法,最终把结果返回给调用者,相当于“服务员帮你协调后厨所有岗位,你只需要点单”。
三、代理模式(Proxy Pattern)(结构型)
1. 定义
为其他对象提供一种代理以控制对这个对象的访问。核心是“控制访问”,可以在不修改原对象的前提下,增加额外功能(如缓存、权限校验、日志记录)。
2. 通俗比喻
你想买房,不需要直接去找房东谈——可以找房产中介。中介就是“代理”,它控制了你对房东的访问:帮你筛选房源、核实房东信息、协商价格、办理过户手续。你只需要和中介对接,原对象(房东)的核心功能(卖房)不变,但多了中介的额外服务。
3. 超简化代码示例(前端场景:图片懒加载代理)
// 原对象:加载图片(核心功能)
class ImageLoader {
load(url) {
const img = new Image();
img.src = url;
document.body.appendChild(img);
}
}
// 代理对象:控制对ImageLoader的访问,增加懒加载功能
class LazyLoadProxy {
constructor() {
this.imageLoader = new ImageLoader();
this.cache = new Map(); // 缓存已加载的图片,避免重复加载
}
load(url) {
// 1. 缓存校验:已加载过的图片直接返回
if (this.cache.has(url)) {
console.log('使用缓存的图片');
return;
}
// 2. 懒加载逻辑:监听滚动,当图片进入视口再加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.imageLoader.load(url);
this.cache.set(url, true); // 加载完成后存入缓存
observer.disconnect(); // 停止监听
}
});
});
// 创建占位符,监听占位符是否进入视口
const placeholder = document.createElement('div');
placeholder.style.width = '300px';
placeholder.style.height = '200px';
placeholder.style.backgroundColor = '#eee';
document.body.appendChild(placeholder);
observer.observe(placeholder);
}
}
// 调用者:使用代理对象,获得懒加载和缓存功能
const proxy = new LazyLoadProxy();
proxy.load('https://picsum.photos/300/200'); // 滚动到占位符位置才加载
4. 图解思路
用流程图展示代理模式的控制逻辑,核心是“代理先拦截请求,处理额外逻辑后再调用原对象”:
解读:调用者调用load方法加载图片时,先经过代理对象LazyLoadProxy。代理会先做两件事:一是检查图片是否已缓存(避免重复加载),二是监听图片是否进入视口(懒加载核心)。只有当图片进入视口且未缓存时,才会调用原对象ImageLoader的load方法真正加载图片,相当于“中介先帮你筛选房源、核实信息,再带你见房东”。
四、工厂模式(Factory Pattern)(创建型)
1. 定义
定义一个创建对象的接口,让子类决定实例化哪一个类。核心是“封装对象的创建过程”,避免在代码中直接使用new关键字,降低耦合度。
2. 通俗比喻
你去奶茶店买奶茶,不需要自己动手做(不需要知道“奶茶”这个对象的创建细节:泡茶、加奶、加糖、加配料),只需要告诉店员“我要珍珠奶茶”或“我要果茶”,店员(工厂)就会给你对应的产品。你只关心产品,不关心创建过程。
3. 超简化代码示例(前端场景:创建不同类型的组件)
// 产品类1:按钮组件
class Button {
render() {
return '<button>按钮</button>';
}
}
// 产品类2:输入框组件
class Input {
render() {
return '<input type="text" placeholder="请输入内容">';
}
}
// 产品类3:下拉框组件
class Select {
render() {
return '<select><option>选项1</option></select>';
}
}
// 工厂类:创建组件的工厂
class ComponentFactory {
createComponent(type) {
switch (type) {
case 'button':
return new Button();
case 'input':
return new Input();
case 'select':
return new Select();
default:
throw new Error('不支持的组件类型');
}
}
}
// 调用者:通过工厂创建组件,无需直接new
const factory = new ComponentFactory();
const button = factory.createComponent('button');
const input = factory.createComponent('input');
console.log(button.render()); // <button>按钮</button>
console.log(input.render()); // <input type="text" placeholder="请输入内容">
4. 图解思路
用流程图展示工厂模式的创建逻辑,核心是“按类型统一创建对象,不用手动new不同类”:
解读:调用者需要创建按钮、输入框等组件时,不用记Button、Input、Select等具体类名并手动new,只需要告诉工厂类ComponentFactory要创建的“类型”(比如button),工厂就会自动创建对应的实例并返回。相当于“你告诉奶茶店员要什么奶茶,店员(工厂)直接给你做好的奶茶,不用你自己动手做”。
五、单例模式(Singleton Pattern)(创建型)
1. 定义
保证一个类只有一个实例,并提供一个访问它的全局访问点。核心是“唯一实例”,避免重复创建对象造成资源浪费(如全局弹窗、缓存对象、浏览器的window对象)。
2. 通俗比喻
一个国家只有一个首都。无论你从哪个城市出发去首都,最终到达的都是同一个地方。这里的“首都”就是单例,“国家”就是提供全局访问点的类。
3. 超简化代码示例(前端场景:全局弹窗组件)
class Dialog {
constructor() {
// 关键:如果已有实例,直接返回,不创建新实例
if (Dialog.instance) {
return Dialog.instance;
}
// 初始化弹窗(创建DOM、设置样式等)
this.element = document.createElement('div');
this.element.style.display = 'none';
this.element.style.position = 'fixed';
this.element.style.top = '50%';
this.element.style.left = '50%';
this.element.style.transform = 'translate(-50%, -50%)';
this.element.style.padding = '20px';
this.element.style.backgroundColor = 'white';
this.element.style.border = '1px solid #ccc';
document.body.appendChild(this.element);
// 保存实例到静态属性
Dialog.instance = this;
}
// 显示弹窗
show(content) {
this.element.innerText = content;
this.element.style.display = 'block';
}
// 隐藏弹窗
hide() {
this.element.style.display = 'none';
}
}
// 测试:多次创建,是否为同一个实例
const dialog1 = new Dialog();
const dialog2 = new Dialog();
console.log(dialog1 === dialog2); // true(同一个实例)
dialog1.show('这是单例模式的弹窗');
// dialog2.hide(); // 调用dialog2的hide也会隐藏同一个弹窗
4. 图解思路
用流程图展示单例模式的实例唯一性逻辑,核心是“每次创建都检查,有则返回旧的,无则创建新的”:
解读:调用者第一次new Dialog时,因为没有实例,会创建新弹窗并把实例存到Dialog.instance里;第二次再new Dialog时,会直接返回之前存的实例,不会再创建新弹窗。这就保证了整个程序里只有一个弹窗对象,相当于“你不管从哪个城市去首都,最终都只能到同一个首都”。
六、策略模式(Strategy Pattern)(行为型)
1. 定义
定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。核心是“算法封装+动态切换”,避免使用大量的if-else或switch-case判断。
2. 通俗比喻
你要去旅行,有多种交通方式(策略)可选:飞机、高铁、汽车。每种方式的实现细节不同(价格、速度、舒适度),但你最终的目的都是“到达目的地”。你可以根据需求动态切换策略,而不需要修改旅行的核心逻辑。
3. 超简化代码示例(前端场景:表单验证策略)
// 策略类1:验证是否为空
class RequiredStrategy {
validate(value) {
return value.trim() !== '' ? '' : '此字段不能为空';
}
}
// 策略类2:验证邮箱格式
class EmailStrategy {
validate(value) {
const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return reg.test(value) ? '' : '请输入正确的邮箱格式';
}
}
// 策略类3:验证密码长度(至少6位)
class PasswordLengthStrategy {
validate(value) {
return value.length >= 6 ? '' : '密码长度至少6位';
}
}
// 上下文类:封装策略的使用,提供统一接口
class Validator {
constructor() {
this.strategies = []; // 存储当前需要使用的策略
}
// 添加策略
add(value, strategy) {
this.strategies.push(() => {
return strategy.validate(value);
});
}
// 执行验证(动态切换策略)
validate() {
for (const validateFunc of this.strategies) {
const errorMsg = validateFunc();
if (errorMsg) {
return errorMsg; // 有错误直接返回
}
}
return '验证通过';
}
}
// 调用者:使用策略模式进行表单验证
const formValidator = new Validator();
// 给用户名添加“非空”策略
formValidator.add('user123', new RequiredStrategy());
// 给邮箱添加“非空”和“邮箱格式”策略
formValidator.add('user@example.com', new RequiredStrategy());
formValidator.add('user@example.com', new EmailStrategy());
// 给密码添加“非空”和“长度”策略
formValidator.add('123456', new RequiredStrategy());
formValidator.add('123456', new PasswordLengthStrategy());
console.log(formValidator.validate()); // 验证通过
4. 图解思路
用流程图展示策略模式的动态切换逻辑,核心是“先收集策略,执行时依次校验,灵活切换”:
解读:调用者先创建验证器Validator,然后给不同字段添加对应的验证策略(比如用户名加“非空”策略,邮箱加“非空+格式”策略)。当调用validate方法时,验证器会依次执行收集的策略,只要有一个策略校验失败就返回错误,全部通过才返回“验证通过”。相当于“你去旅行前选好交通策略,出发时按选好的策略走,想换策略直接换,不用改旅行的核心逻辑”。
七、迭代器模式(Iterator Pattern)(行为型)
1. 定义
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。核心是“统一遍历接口”,无论聚合对象是数组、对象、Set等,都可以用相同的方式遍历。
2. 通俗比喻
你去餐厅吃自助餐,不需要知道餐厅的食材仓库是怎么存放的(内部表示),只需要沿着取餐台(迭代器)依次取餐即可。取餐台提供了“下一个”的统一接口,你不需要关心食材的存储结构。
3. 超简化代码示例(前端场景:遍历对象和数组的统一迭代器)
// 迭代器类:提供统一的遍历接口(hasNext、next)
class Iterator {
constructor(collection) {
this.collection = collection; // 聚合对象(数组/对象)
this.index = 0; // 遍历索引
}
// 判断是否还有下一个元素
hasNext() {
if (Array.isArray(this.collection)) {
return this.index < this.collection.length;
} else {
// 处理对象:获取对象的key数组
const keys = Object.keys(this.collection);
return this.index < keys.length;
}
}
// 获取下一个元素
next() {
if (Array.isArray(this.collection)) {
return this.collection[this.index++];
} else {
const keys = Object.keys(this.collection);
return this.collection[keys[this.index++]];
}
}
}
// 调用者:用统一接口遍历不同聚合对象
// 1. 遍历数组
const arr = [1, 2, 3, 4];
const arrIterator = new Iterator(arr);
while (arrIterator.hasNext()) {
console.log(arrIterator.next()); // 1, 2, 3, 4
}
// 2. 遍历对象
const obj = { name: '张三', age: 20, gender: '男' };
const objIterator = new Iterator(obj);
while (objIterator.hasNext()) {
console.log(objIterator.next()); // 张三, 20, 男
}
4. 图解思路
用流程图展示迭代器模式的统一遍历逻辑,核心是“不管数据结构是数组还是对象,都用相同方法遍历”:
解读:调用者不管传入的是数组还是对象,都通过hasNext(判断是否有下一个元素)和next(获取下一个元素)两个统一接口遍历。迭代器内部会自动适配不同数据结构的遍历规则,调用者不用关心数组的length属性或对象的key数组,相当于“你去自助餐取餐,不管食材是放在盘子里还是盒子里,都沿着取餐台依次拿,不用关心食材的存储方式”。
八、观察者模式(Observer Pattern)(行为型)
1. 定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。核心是“发布-订阅”,解耦发布者和订阅者。
2. 通俗比喻
你关注了一个公众号(发布者),当公众号发布新文章(状态改变)时,所有关注它的用户(订阅者)都会收到推送通知。公众号不需要知道具体有哪些用户关注,用户也不需要时刻刷新公众号——发布者状态改变时,订阅者自动接收消息。
3. 超简化代码示例(前端场景:全局状态通知)
// 发布者类(公众号)
class Publisher {
constructor() {
this.subscribers = []; // 存储订阅者
}
// 订阅:添加订阅者
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
// 取消订阅:移除订阅者
unsubscribe(subscriber) {
this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
}
// 发布:状态改变时通知所有订阅者
publish(message) {
this.subscribers.forEach(subscriber => {
subscriber.update(message); // 调用订阅者的更新方法
});
}
}
// 订阅者类1(用户A)
class SubscriberA {
update(message) {
console.log('用户A收到消息:', message);
}
}
// 订阅者类2(用户B)
class SubscriberB {
update(message) {
console.log('用户B收到消息:', message);
}
}
// 调用者:模拟发布-订阅流程
const publisher = new Publisher();
const subscriberA = new SubscriberA();
const subscriberB = new SubscriberB();
// 订阅
publisher.subscribe(subscriberA);
publisher.subscribe(subscriberB);
// 发布消息
publisher.publish('前端设计模式干货文章更新啦!');
// 输出:用户A收到消息:前端设计模式干货文章更新啦!
// 输出:用户B收到消息:前端设计模式干货文章更新啦!
// 取消订阅(用户B)
publisher.unsubscribe(subscriberB);
publisher.publish('下一篇文章:React Hooks实战');
// 输出:用户A收到消息:下一篇文章:React Hooks实战
4. 图解思路
用流程图展示观察者模式的发布-订阅逻辑,核心是“发布者状态变了,自动通知所有订阅者”:
解读:订阅者A和B先关注(订阅)发布者,发布者会把它们存到订阅者列表里。当发布者状态改变(比如发布新文章)时,会调用publish方法遍历列表,自动通知每个订阅者执行update方法处理消息。相当于“你关注的公众号发新文章,公众号会自动给所有关注的用户推送,不用你手动刷新”。
九、中介者模式(Mediator Pattern)(行为型)
1. 定义
用一个中介者对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。核心是“解耦对象间的直接交互”,通过中介者统一协调。
2. 通俗比喻
机场的塔台(中介者)协调不同飞机的起飞和降落。如果没有塔台,飞机之间需要直接沟通(“我要起飞,你是否在跑道上?”“你要降落,我是否需要避让?”),耦合度极高。有了塔台后,飞机只需要和塔台沟通,塔台统一协调所有飞机的行为,飞机之间无需直接交互。
3. 超简化代码示例(前端场景:组件间通信的中介者)
// 中介者类:协调组件间的通信
class Mediator {
constructor() {
this.components = {}; // 存储需要协调的组件
}
// 注册组件
register(componentName, component) {
this.components[componentName] = component;
component.setMediator(this); // 给组件绑定中介者
}
// 转发消息:组件通过中介者发送消息给其他组件
send(message, fromComponent, toComponentName) {
if (toComponentName && this.components[toComponentName]) {
// 定向发送给指定组件
this.components[toComponentName].receive(message, fromComponent);
} else {
// 广播给所有组件(除了发送者)
Object.keys(this.components).forEach(name => {
if (name !== fromComponent.name) {
this.components[name].receive(message, fromComponent);
}
});
}
}
}
// 组件类1:头部组件
class HeaderComponent {
constructor(name) {
this.name = name;
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
// 发送消息
sendMessage(message, toComponentName) {
this.mediator.send(message, this, toComponentName);
}
// 接收消息
receive(message, fromComponent) {
console.log(`[${this.name}] 收到来自 [${fromComponent.name}] 的消息:${message}`);
}
}
// 组件类2:内容组件
class ContentComponent {
constructor(name) {
this.name = name;
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
sendMessage(message, toComponentName) {
this.mediator.send(message, this, toComponentName);
}
receive(message, fromComponent) {
console.log(`[${this.name}] 收到来自 [${fromComponent.name}] 的消息:${message}`);
}
}
// 组件类3:底部组件
class FooterComponent {
constructor(name) {
this.name = name;
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
sendMessage(message, toComponentName) {
this.mediator.send(message, this, toComponentName);
}
receive(message, fromComponent) {
console.log(`[${this.name}] 收到来自 [${fromComponent.name}] 的消息:${message}`);
}
}
// 调用者:通过中介者协调组件通信
const mediator = new Mediator();
const header = new HeaderComponent('Header');
const content = new ContentComponent('Content');
const footer = new FooterComponent('Footer');
// 注册组件
mediator.register('Header', header);
mediator.register('Content', content);
mediator.register('Footer', footer);
// 1. Header定向发送消息给Content
header.sendMessage('请加载首页内容', 'Content');
// 输出:[Content] 收到来自 [Header] 的消息:请加载首页内容
// 2. Content广播消息(所有组件都能收到,除了自己)
content.sendMessage('首页内容加载完成');
// 输出:[Header] 收到来自 [Content] 的消息:首页内容加载完成
// 输出:[Footer] 收到来自 [Content] 的消息:首页内容加载完成
4. 图解思路
用流程图展示中介者模式的协调逻辑,核心是“组件不直接通信,都通过中介者转发消息”:
解读:Header、Content、Footer组件先注册到中介者,中介者会管理所有组件。当Header想给Content发消息时,不会直接找Content,而是把消息发给中介者,由中介者转发给Content;Content广播消息时,也是通过中介者转发给其他所有组件。相当于“飞机之间不直接沟通,都通过塔台(中介者)协调起飞降落,避免混乱”。
十、访问者模式(Visitor Pattern)(行为型)
1. 定义
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。核心是“分离数据结构和操作”,新增操作时不需要修改原有数据结构的代码。
2. 通俗比喻
博物馆里有很多展品(数据结构元素:绘画、雕塑、文物),不同的参观者(访问者:普通游客、专家、摄影师)对展品有不同的操作(普通游客:观看讲解;专家:研究细节;摄影师:拍照记录)。展品的结构(绘画、雕塑)不变,但可以新增不同的参观者(新操作),而不需要修改展品的代码。
3. 超简化代码示例(前端场景:对不同类型数据的不同操作)
// 数据结构元素1:用户数据
class UserData {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 接受访问者的访问
accept(visitor) {
visitor.visitUserData(this);
}
}
// 数据结构元素2:商品数据
class ProductData {
constructor(name, price) {
this.name = name;
this.price = price;
}
// 接受访问者的访问
accept(visitor) {
visitor.visitProductData(this);
}
}
// 访问者1:导出数据为JSON格式(操作1)
class JsonExportVisitor {
visitUserData(user) {
return JSON.stringify({ type: 'user', data: user });
}
visitProductData(product) {
return JSON.stringify({ type: 'product', data: product });
}
}
// 访问者2:导出数据为CSV格式(操作2,新增操作无需修改数据结构)
class CsvExportVisitor {
visitUserData(user) {
return `user,${user.name},${user.age}`;
}
visitProductData(product) {
return `product,${product.name},${product.price}`;
}
}
// 调用者:使用不同访问者操作数据
const user = new UserData('张三', 20);
const product = new ProductData('手机', 5999);
// 1. 导出为JSON
const jsonVisitor = new JsonExportVisitor();
console.log(user.accept(jsonVisitor)); // {"type":"user","data":{"name":"张三","age":20}}
console.log(product.accept(jsonVisitor)); // {"type":"product","data":{"name":"手机","price":5999}}
// 2. 导出为CSV(新增操作,未修改UserData和ProductData)
const csvVisitor = new CsvExportVisitor();
console.log(user.accept(csvVisitor)); // user,张三,20
console.log(product.accept(csvVisitor)); // product,手机,5999
4. 图解思路
用流程图展示访问者模式的分离逻辑,核心是“数据结构和操作分开,新增操作不用改数据”:
解读:UserData和ProductData是数据结构,它们只负责存储数据,不负责处理数据的操作(比如导出)。导出JSON、CSV等操作都封装在访问者里,数据结构通过accept方法接受访问者访问,访问者会调用对应方法处理数据。想新增导出Excel的操作,只需新建一个访问者类,不用修改UserData和ProductData,相当于“博物馆展品(数据)不负责讲解,不同参观者(访问者)有不同的讲解方式,新增参观者不用改展品”。
十一、9种前端设计模式优缺点对比表格
| 设计模式 | 优点 | 缺点 | 前端适用场景 |
|---|---|---|---|
| 外观模式 | 1. 简化接口,降低使用成本;2. 封装复杂性,隔离子系统变化;3. 减少调用者与子系统的耦合 | 1. 外观类可能变得过于庞大,承担过多职责;2. 新增子系统功能时,可能需要修改外观类 | 封装复杂DOM操作、整合多个API接口、组件库的统一入口 |
| 代理模式 | 1. 控制访问,灵活添加额外功能(缓存、权限、日志);2. 不修改原对象,符合开闭原则;3. 解耦调用者与原对象 | 1. 增加了代理层,可能导致请求处理速度变慢;2. 代码结构变得更复杂(多了代理类) | 图片懒加载、接口请求缓存、权限控制、日志埋点 |
| 工厂模式 | 1. 封装对象创建过程,降低耦合;2. 便于统一管理对象创建;3. 新增产品时,只需新增产品类和修改工厂,符合开闭原则 | 1. 增加了代码复杂度(多了工厂类和产品类);2. 简单场景下显得冗余 | 组件库创建不同类型组件、表单元素生成、路由配置生成 |
| 单例模式 | 1. 保证唯一实例,节省资源;2. 提供全局访问点,方便使用;3. 避免重复创建对象导致的状态不一致 | 1. 单例类职责过重,违背单一职责原则;2. 多线程环境下需要额外处理线程安全问题;3. 测试困难(依赖全局状态) | 全局弹窗、缓存对象、全局状态管理(如Vuex的Store)、浏览器window对象 |
| 策略模式 | 1. 消除大量if-else/switch-case;2. 策略可动态切换,灵活性高;3. 新增策略只需新增类,符合开闭原则 | 1. 策略类数量可能过多;2. 调用者需要了解所有策略的差异,增加使用成本 | 表单验证、动态排序、主题切换、支付方式选择 |
| 迭代器模式 | 1. 统一遍历接口,屏蔽不同聚合对象的差异;2. 简化遍历逻辑,便于代码复用;3. 支持多种遍历方式(正序、倒序) | 1. 简单遍历场景下显得冗余;2. 增加了代码复杂度(多了迭代器类) | 遍历不同数据结构(数组、对象、Set、Map)、表格数据遍历、列表渲染 |
| 观察者模式 | 1. 解耦发布者和订阅者;2. 支持一对多通信,便于状态同步;3. 订阅者可动态添加/移除,灵活性高 | 1. 订阅者过多时,通知效率低;2. 发布者与订阅者间存在隐式依赖,调试困难;3. 可能导致循环依赖 | 全局状态通知、事件总线(EventBus)、组件间通信、消息推送 |
| 中介者模式 | 1. 解耦多个对象间的直接交互;2. 集中管理对象间的通信逻辑,便于维护;3. 简化对象间的关系 | 1. 中介者类可能变得过于复杂,成为“万能类”;2. 中介者故障会影响所有对象 | 多组件间复杂通信、页面状态协调、游戏场景中角色间交互 |
| 访问者模式 | 1. 分离数据结构和操作,新增操作无需修改数据结构;2. 集中管理不同操作,便于维护;3. 支持对同一数据结构进行多种不同操作 | 1. 数据结构变化时,需要修改所有访问者类;2. 增加了代码复杂度(多了访问者类和accept方法);3. 访问者可能需要访问数据结构的内部状态,破坏封装 | 数据导出(多种格式)、数据校验(多种规则)、报表生成、复杂数据的多维度处理 |
十二、总结
设计模式的核心不是“死记硬背”,而是“理解思想”——通过封装、解耦、抽象,让代码更具可维护性、可扩展性和可复用性。
最后再提一句:没有最好的设计模式,只有最适合的场景。简单场景下无需过度设计,复杂场景下合理运用设计模式,才能真正提升开发效率。
如果这篇文章对你有帮助,别忘了点赞+收藏+关注~ 后续会分享更多前端进阶干货!
(注:文档部分内容可能由 AI 生成)
一、先搞懂:9大设计模式的3大分类
很多同学学设计模式会混乱,其实按“核心作用”可以把9大模式分成3类,对应软件设计的3个核心问题,先分清类别再学,思路会清晰很多:
| 分类 | 核心作用(解决什么问题) | 包含的设计模式 | 通俗比喻 |
|---|---|---|---|
| 创建型模式 | 封装对象的创建过程,让对象创建更灵活、更规范,降低创建对象的耦合度 | 工厂模式、单例模式 | 相当于“奶茶店的后厨”:负责按需求创建奶茶(对象),你不用管奶茶怎么制作(创建细节) |
| 结构型模式 | 处理类或对象的组合关系,通过合理的结构组合,让系统更灵活、可扩展 | 外观模式、代理模式 | 相当于“房屋的装修结构”:把不同的建材(类/对象)按合理方式组合,满足不同需求(功能) |
| 行为型模式 | 定义对象间的交互规则和职责分配,让对象间的通信更清晰、更解耦 | 策略模式、迭代器模式、观察者模式、中介者模式、访问者模式 | 相当于“公司的部门协作规则”:明确不同部门(对象)的职责和沟通方式,避免混乱 |
| 补充说明:不同资料对设计模式的分类可能有细微差异,但核心逻辑一致。这里按前端最常用的分类方式,聚焦“实用场景”而非纯理论,方便大家理解和应用。 |
二、外观模式(Facade Pattern)(结构型)
1. 定义
为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用。核心是“封装复杂性,提供简洁接口”。
2. 通俗比喻
你去餐厅吃饭,不需要直接找后厨的厨师、配菜员、洗碗工——只需要找服务员点单即可。这里的“服务员”就是外观模式的核心,它封装了“点餐-做菜-上菜-结账”的整个复杂流程,给你提供了“点单”和“结账”两个简单接口。
3. 超简化代码示例(前端场景:封装复杂的DOM操作)
// 子系统:复杂的DOM操作(单独写会很繁琐)
function createDiv(content) {
const div = document.createElement('div');
div.innerText = content;
return div;
}
function setStyle(element, style) {
for (let key in style) {
element.style[key] = style[key];
}
}
function appendToBody(element) {
document.body.appendChild(element);
}
// 外观类:提供统一接口
class DOMFacade {
// 统一接口:创建带样式的元素并添加到body
createAndAppendDiv(content, style) {
const div = createDiv(content);
setStyle(div, style);
appendToBody(div);
}
}
// 调用者:直接使用外观接口,无需关注子系统细节
const domHelper = new DOMFacade();
domHelper.createAndAppendDiv('外观模式示例', { color: 'red', fontSize: '20px' });
4. 图解思路
用流程图展示外观模式的交互逻辑,核心是“调用者只对接外观类,不用管子系统细节”:
解读:调用者想完成“创建带样式的div并添加到页面”,不用分别调用createDiv、setStyle、appendToBody三个方法,只需要调用外观类DOMFacade的createAndAppendDiv接口即可。外观类内部会自动协调三个子系统方法,最终把结果返回给调用者,相当于“服务员帮你协调后厨所有岗位,你只需要点单”。
graph TD
A[调用者] -- 调用简洁接口 --> B[外观类(DOMFacade)]
B -- 内部调用子系统接口 --> C[createDiv]
B -- 内部调用子系统接口 --> D[setStyle]
B -- 内部调用子系统接口 --> E[appendToBody]
C --> F[创建div元素]
D --> G[设置元素样式]
E --> H[添加到body]
F & G & H --> I[完成操作,返回结果给调用者]
三、代理模式(Proxy Pattern)(结构型)
1. 定义
为其他对象提供一种代理以控制对这个对象的访问。核心是“控制访问”,可以在不修改原对象的前提下,增加额外功能(如缓存、权限校验、日志记录)。
2. 通俗比喻
你想买房,不需要直接去找房东谈——可以找房产中介。中介就是“代理”,它控制了你对房东的访问:帮你筛选房源、核实房东信息、协商价格、办理过户手续。你只需要和中介对接,原对象(房东)的核心功能(卖房)不变,但多了中介的额外服务。
3. 超简化代码示例(前端场景:图片懒加载代理)
// 原对象:加载图片(核心功能)
class ImageLoader {
load(url) {
const img = new Image();
img.src = url;
document.body.appendChild(img);
}
}
// 代理对象:控制对ImageLoader的访问,增加懒加载功能
class LazyLoadProxy {
constructor() {
this.imageLoader = new ImageLoader();
this.cache = new Map(); // 缓存已加载的图片,避免重复加载
}
load(url) {
// 1. 缓存校验:已加载过的图片直接返回
if (this.cache.has(url)) {
console.log('使用缓存的图片');
return;
}
// 2. 懒加载逻辑:监听滚动,当图片进入视口再加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.imageLoader.load(url);
this.cache.set(url, true); // 加载完成后存入缓存
observer.disconnect(); // 停止监听
}
});
});
// 创建占位符,监听占位符是否进入视口
const placeholder = document.createElement('div');
placeholder.style.width = '300px';
placeholder.style.height = '200px';
placeholder.style.backgroundColor = '#eee';
document.body.appendChild(placeholder);
observer.observe(placeholder);
}
}
// 调用者:使用代理对象,获得懒加载和缓存功能
const proxy = new LazyLoadProxy();
proxy.load('https://picsum.photos/300/200'); // 滚动到占位符位置才加载
4. 图解思路
用流程图展示代理模式的控制逻辑,核心是“代理先拦截请求,处理额外逻辑后再调用原对象”:
解读:调用者调用load方法加载图片时,先经过代理对象LazyLoadProxy。代理会先做两件事:一是检查图片是否已缓存(避免重复加载),二是监听图片是否进入视口(懒加载核心)。只有当图片进入视口且未缓存时,才会调用原对象ImageLoader的load方法真正加载图片,相当于“中介先帮你筛选房源、核实信息,再带你见房东”。
graph TD
A[调用者] -- 调用load方法 --> B[代理对象(LazyLoadProxy)]
B -- 1. 缓存校验 --> C{是否已缓存?}
C -- 是 --> D[直接返回缓存结果]
C -- 否 --> E[2. 创建占位符,监听滚动]
E -- 3. 监听视口 --> F{是否进入视口?}
F -- 否 --> E[继续监听]
F -- 是 --> G[调用原对象的load方法]
G -- 4. 加载图片 --> H[存入缓存]
H --> I[完成加载,返回结果]
四、工厂模式(Factory Pattern)(创建型)
1. 定义
定义一个创建对象的接口,让子类决定实例化哪一个类。核心是“封装对象的创建过程”,避免在代码中直接使用new关键字,降低耦合度。
2. 通俗比喻
你去奶茶店买奶茶,不需要自己动手做(不需要知道“奶茶”这个对象的创建细节:泡茶、加奶、加糖、加配料),只需要告诉店员“我要珍珠奶茶”或“我要果茶”,店员(工厂)就会给你对应的产品。你只关心产品,不关心创建过程。
3. 超简化代码示例(前端场景:创建不同类型的组件)
// 产品类1:按钮组件
class Button {
render() {
return '<button>按钮</button>';
}
}
// 产品类2:输入框组件
class Input {
render() {
return '<input type="text" placeholder="请输入内容">';
}
}
// 产品类3:下拉框组件
class Select {
render() {
return '<select><option>选项1</option></select>';
}
}
// 工厂类:创建组件的工厂
class ComponentFactory {
createComponent(type) {
switch (type) {
case 'button':
return new Button();
case 'input':
return new Input();
case 'select':
return new Select();
default:
throw new Error('不支持的组件类型');
}
}
}
// 调用者:通过工厂创建组件,无需直接new
const factory = new ComponentFactory();
const button = factory.createComponent('button');
const input = factory.createComponent('input');
console.log(button.render()); // <button>按钮</button>
console.log(input.render()); // <input type="text" placeholder="请输入内容">
4. 图解思路
用流程图展示工厂模式的创建逻辑,核心是“按类型统一创建对象,不用手动new不同类”:
解读:调用者需要创建按钮、输入框等组件时,不用记Button、Input、Select等具体类名并手动new,只需要告诉工厂类ComponentFactory要创建的“类型”(比如button),工厂就会自动创建对应的实例并返回。相当于“你告诉奶茶店员要什么奶茶,店员(工厂)直接给你做好的奶茶,不用你自己动手做”。
graph TD
A[调用者] -- 传入类型(如button) --> B[工厂类(ComponentFactory)]
B -- 根据类型判断 --> C{组件类型}
C -- button --> D[创建Button实例]
C -- input --> E[创建Input实例]
C -- select --> F[创建Select实例]
D & E & F -- 返回实例 --> G[调用者使用实例(如render)]
五、单例模式(Singleton Pattern)(创建型)
1. 定义
保证一个类只有一个实例,并提供一个访问它的全局访问点。核心是“唯一实例”,避免重复创建对象造成资源浪费(如全局弹窗、缓存对象、浏览器的window对象)。
2. 通俗比喻
一个国家只有一个首都。无论你从哪个城市出发去首都,最终到达的都是同一个地方。这里的“首都”就是单例,“国家”就是提供全局访问点的类。
3. 超简化代码示例(前端场景:全局弹窗组件)
class Dialog {
constructor() {
// 关键:如果已有实例,直接返回,不创建新实例
if (Dialog.instance) {
return Dialog.instance;
}
// 初始化弹窗(创建DOM、设置样式等)
this.element = document.createElement('div');
this.element.style.display = 'none';
this.element.style.position = 'fixed';
this.element.style.top = '50%';
this.element.style.left = '50%';
this.element.style.transform = 'translate(-50%, -50%)';
this.element.style.padding = '20px';
this.element.style.backgroundColor = 'white';
this.element.style.border = '1px solid #ccc';
document.body.appendChild(this.element);
// 保存实例到静态属性
Dialog.instance = this;
}
// 显示弹窗
show(content) {
this.element.innerText = content;
this.element.style.display = 'block';
}
// 隐藏弹窗
hide() {
this.element.style.display = 'none';
}
}
// 测试:多次创建,是否为同一个实例
const dialog1 = new Dialog();
const dialog2 = new Dialog();
console.log(dialog1 === dialog2); // true(同一个实例)
dialog1.show('这是单例模式的弹窗');
// dialog2.hide(); // 调用dialog2的hide也会隐藏同一个弹窗
4. 图解思路
用流程图展示单例模式的实例唯一性逻辑,核心是“每次创建都检查,有则返回旧的,无则创建新的”:
解读:调用者第一次new Dialog时,因为没有实例,会创建新弹窗并把实例存到Dialog.instance里;第二次再new Dialog时,会直接返回之前存的实例,不会再创建新弹窗。这就保证了整个程序里只有一个弹窗对象,相当于“你不管从哪个城市去首都,最终都只能到同一个首都”。
graph TD
A[调用者] -- 第一次new Dialog() --> B[Dialog类]
B -- 检查静态属性instance --> C{是否存在实例?}
C -- 否 --> D[创建新实例,初始化弹窗]
D --> E[将实例存入Dialog.instance]
E --> F[返回实例]
A -- 第二次new Dialog() --> B
C -- 是 --> F[直接返回已有的instance]
F --> G[调用者使用实例(show/hide)]
六、策略模式(Strategy Pattern)(行为型)
1. 定义
定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。核心是“算法封装+动态切换”,避免使用大量的if-else或switch-case判断。
2. 通俗比喻
你要去旅行,有多种交通方式(策略)可选:飞机、高铁、汽车。每种方式的实现细节不同(价格、速度、舒适度),但你最终的目的都是“到达目的地”。你可以根据需求动态切换策略,而不需要修改旅行的核心逻辑。
3. 超简化代码示例(前端场景:表单验证策略)
// 策略类1:验证是否为空
class RequiredStrategy {
validate(value) {
return value.trim() !== '' ? '' : '此字段不能为空';
}
}
// 策略类2:验证邮箱格式
class EmailStrategy {
validate(value) {
const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return reg.test(value) ? '' : '请输入正确的邮箱格式';
}
}
// 策略类3:验证密码长度(至少6位)
class PasswordLengthStrategy {
validate(value) {
return value.length >= 6 ? '' : '密码长度至少6位';
}
}
// 上下文类:封装策略的使用,提供统一接口
class Validator {
constructor() {
this.strategies = []; // 存储当前需要使用的策略
}
// 添加策略
add(value, strategy) {
this.strategies.push(() => {
return strategy.validate(value);
});
}
// 执行验证(动态切换策略)
validate() {
for (const validateFunc of this.strategies) {
const errorMsg = validateFunc();
if (errorMsg) {
return errorMsg; // 有错误直接返回
}
}
return '验证通过';
}
}
// 调用者:使用策略模式进行表单验证
const formValidator = new Validator();
// 给用户名添加“非空”策略
formValidator.add('user123', new RequiredStrategy());
// 给邮箱添加“非空”和“邮箱格式”策略
formValidator.add('user@example.com', new RequiredStrategy());
formValidator.add('user@example.com', new EmailStrategy());
// 给密码添加“非空”和“长度”策略
formValidator.add('123456', new RequiredStrategy());
formValidator.add('123456', new PasswordLengthStrategy());
console.log(formValidator.validate()); // 验证通过
4. 图解思路
用流程图展示策略模式的动态切换逻辑,核心是“先收集策略,执行时依次校验,灵活切换”:
解读:调用者先创建验证器Validator,然后给不同字段添加对应的验证策略(比如用户名加“非空”策略,邮箱加“非空+格式”策略)。当调用validate方法时,验证器会依次执行收集的策略,只要有一个策略校验失败就返回错误,全部通过才返回“验证通过”。相当于“你去旅行前选好交通策略,出发时按选好的策略走,想换策略直接换,不用改旅行的核心逻辑”。
graph TD
A[调用者] -- 1. 创建验证器 --> B[上下文类(Validator)]
A -- 2. 添加策略(如Required、Email) --> B
B -- 存储策略到数组 --> C[strategies数组]
A -- 3. 调用validate() --> B
B -- 遍历策略数组 --> D[执行第一个策略的validate]
D -- 有错误 --> E[返回错误信息]
D -- 无错误 --> F[执行第二个策略的validate]
F -- 有错误 --> E
F -- 无错误 --> G[...依次执行所有策略]
G -- 所有策略无错误 --> H[返回“验证通过”]
七、迭代器模式(Iterator Pattern)(行为型)
1. 定义
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。核心是“统一遍历接口”,无论聚合对象是数组、对象、Set等,都可以用相同的方式遍历。
2. 通俗比喻
你去餐厅吃自助餐,不需要知道餐厅的食材仓库是怎么存放的(内部表示),只需要沿着取餐台(迭代器)依次取餐即可。取餐台提供了“下一个”的统一接口,你不需要关心食材的存储结构。
3. 超简化代码示例(前端场景:遍历对象和数组的统一迭代器)
// 迭代器类:提供统一的遍历接口(hasNext、next)
class Iterator {
constructor(collection) {
this.collection = collection; // 聚合对象(数组/对象)
this.index = 0; // 遍历索引
}
// 判断是否还有下一个元素
hasNext() {
if (Array.isArray(this.collection)) {
return this.index < this.collection.length;
} else {
// 处理对象:获取对象的key数组
const keys = Object.keys(this.collection);
return this.index < keys.length;
}
}
// 获取下一个元素
next() {
if (Array.isArray(this.collection)) {
return this.collection[this.index++];
} else {
const keys = Object.keys(this.collection);
return this.collection[keys[this.index++]];
}
}
}
// 调用者:用统一接口遍历不同聚合对象
// 1. 遍历数组
const arr = [1, 2, 3, 4];
const arrIterator = new Iterator(arr);
while (arrIterator.hasNext()) {
console.log(arrIterator.next()); // 1, 2, 3, 4
}
// 2. 遍历对象
const obj = { name: '张三', age: 20, gender: '男' };
const objIterator = new Iterator(obj);
while (objIterator.hasNext()) {
console.log(objIterator.next()); // 张三, 20, 男
}
4. 图解思路
用流程图展示迭代器模式的统一遍历逻辑,核心是“不管数据结构是数组还是对象,都用相同方法遍历”:
解读:调用者不管传入的是数组还是对象,都通过hasNext(判断是否有下一个元素)和next(获取下一个元素)两个统一接口遍历。迭代器内部会自动适配不同数据结构的遍历规则,调用者不用关心数组的length属性或对象的key数组,相当于“你去自助餐取餐,不管食材是放在盘子里还是盒子里,都沿着取餐台依次拿,不用关心食材的存储方式”。
graph TD
A[调用者] -- 传入聚合对象(数组/对象) --> B[迭代器类(Iterator)]
A -- 调用hasNext() --> C{是否有下一个元素?}
C -- 否 --> D[遍历结束]
C -- 是 --> E[调用next()获取元素]
E -- 输出元素 --> F[索引自增]
F --> C[再次调用hasNext()]
八、观察者模式(Observer Pattern)(行为型)
1. 定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。核心是“发布-订阅”,解耦发布者和订阅者。
2. 通俗比喻
你关注了一个公众号(发布者),当公众号发布新文章(状态改变)时,所有关注它的用户(订阅者)都会收到推送通知。公众号不需要知道具体有哪些用户关注,用户也不需要时刻刷新公众号——发布者状态改变时,订阅者自动接收消息。
3. 超简化代码示例(前端场景:全局状态通知)
// 发布者类(公众号)
class Publisher {
constructor() {
this.subscribers = []; // 存储订阅者
}
// 订阅:添加订阅者
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
// 取消订阅:移除订阅者
unsubscribe(subscriber) {
this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
}
// 发布:状态改变时通知所有订阅者
publish(message) {
this.subscribers.forEach(subscriber => {
subscriber.update(message); // 调用订阅者的更新方法
});
}
}
// 订阅者类1(用户A)
class SubscriberA {
update(message) {
console.log('用户A收到消息:', message);
}
}
// 订阅者类2(用户B)
class SubscriberB {
update(message) {
console.log('用户B收到消息:', message);
}
}
// 调用者:模拟发布-订阅流程
const publisher = new Publisher();
const subscriberA = new SubscriberA();
const subscriberB = new SubscriberB();
// 订阅
publisher.subscribe(subscriberA);
publisher.subscribe(subscriberB);
// 发布消息
publisher.publish('前端设计模式干货文章更新啦!');
// 输出:用户A收到消息:前端设计模式干货文章更新啦!
// 输出:用户B收到消息:前端设计模式干货文章更新啦!
// 取消订阅(用户B)
publisher.unsubscribe(subscriberB);
publisher.publish('下一篇文章:React Hooks实战');
// 输出:用户A收到消息:下一篇文章:React Hooks实战
4. 图解思路
用流程图展示观察者模式的发布-订阅逻辑,核心是“发布者状态变了,自动通知所有订阅者”:
解读:订阅者A和B先关注(订阅)发布者,发布者会把它们存到订阅者列表里。当发布者状态改变(比如发布新文章)时,会调用publish方法遍历列表,自动通知每个订阅者执行update方法处理消息。相当于“你关注的公众号发新文章,公众号会自动给所有关注的用户推送,不用你手动刷新”。
graph TD
A[订阅者A] -- 订阅 --> B[发布者(Publisher)]
C[订阅者B] -- 订阅 --> B
B -- 存储订阅者到数组 --> D[subscribers数组]
E[发布者状态改变] -- 调用publish() --> B
B -- 遍历订阅者数组 --> F[调用订阅者A的update()]
B -- 遍历订阅者数组 --> G[调用订阅者B的update()]
F --> H[订阅者A处理消息]
G --> I[订阅者B处理消息]
九、中介者模式(Mediator Pattern)(行为型)
1. 定义
用一个中介者对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。核心是“解耦对象间的直接交互”,通过中介者统一协调。
2. 通俗比喻
机场的塔台(中介者)协调不同飞机的起飞和降落。如果没有塔台,飞机之间需要直接沟通(“我要起飞,你是否在跑道上?”“你要降落,我是否需要避让?”),耦合度极高。有了塔台后,飞机只需要和塔台沟通,塔台统一协调所有飞机的行为,飞机之间无需直接交互。
3. 超简化代码示例(前端场景:组件间通信的中介者)
// 中介者类:协调组件间的通信
class Mediator {
constructor() {
this.components = {}; // 存储需要协调的组件
}
// 注册组件
register(componentName, component) {
this.components[componentName] = component;
component.setMediator(this); // 给组件绑定中介者
}
// 转发消息:组件通过中介者发送消息给其他组件
send(message, fromComponent, toComponentName) {
if (toComponentName && this.components[toComponentName]) {
// 定向发送给指定组件
this.components[toComponentName].receive(message, fromComponent);
} else {
// 广播给所有组件(除了发送者)
Object.keys(this.components).forEach(name => {
if (name !== fromComponent.name) {
this.components[name].receive(message, fromComponent);
}
});
}
}
}
// 组件类1:头部组件
class HeaderComponent {
constructor(name) {
this.name = name;
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
// 发送消息
sendMessage(message, toComponentName) {
this.mediator.send(message, this, toComponentName);
}
// 接收消息
receive(message, fromComponent) {
console.log(`[${this.name}] 收到来自 [${fromComponent.name}] 的消息:${message}`);
}
}
// 组件类2:内容组件
class ContentComponent {
constructor(name) {
this.name = name;
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
sendMessage(message, toComponentName) {
this.mediator.send(message, this, toComponentName);
}
receive(message, fromComponent) {
console.log(`[${this.name}] 收到来自 [${fromComponent.name}] 的消息:${message}`);
}
}
// 组件类3:底部组件
class FooterComponent {
constructor(name) {
this.name = name;
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
sendMessage(message, toComponentName) {
this.mediator.send(message, this, toComponentName);
}
receive(message, fromComponent) {
console.log(`[${this.name}] 收到来自 [${fromComponent.name}] 的消息:${message}`);
}
}
// 调用者:通过中介者协调组件通信
const mediator = new Mediator();
const header = new HeaderComponent('Header');
const content = new ContentComponent('Content');
const footer = new FooterComponent('Footer');
// 注册组件
mediator.register('Header', header);
mediator.register('Content', content);
mediator.register('Footer', footer);
// 1. Header定向发送消息给Content
header.sendMessage('请加载首页内容', 'Content');
// 输出:[Content] 收到来自 [Header] 的消息:请加载首页内容
// 2. Content广播消息(所有组件都能收到,除了自己)
content.sendMessage('首页内容加载完成');
// 输出:[Header] 收到来自 [Content] 的消息:首页内容加载完成
// 输出:[Footer] 收到来自 [Content] 的消息:首页内容加载完成
4. 图解思路
用流程图展示中介者模式的协调逻辑,核心是“组件不直接通信,都通过中介者转发消息”:
解读:Header、Content、Footer组件先注册到中介者,中介者会管理所有组件。当Header想给Content发消息时,不会直接找Content,而是把消息发给中介者,由中介者转发给Content;Content广播消息时,也是通过中介者转发给其他所有组件。相当于“飞机之间不直接沟通,都通过塔台(中介者)协调起飞降落,避免混乱”。
graph TD
A[Header组件] -- 注册 --> B[中介者(Mediator)]
C[Content组件] -- 注册 --> B
D[Footer组件] -- 注册 --> B
A -- 发送消息给Content --> B
B -- 转发消息 --> C
C -- 广播消息 --> B
B -- 转发消息给Header --> A
B -- 转发消息给Footer --> D
十、访问者模式(Visitor Pattern)(行为型)
1. 定义
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。核心是“分离数据结构和操作”,新增操作时不需要修改原有数据结构的代码。
2. 通俗比喻
博物馆里有很多展品(数据结构元素:绘画、雕塑、文物),不同的参观者(访问者:普通游客、专家、摄影师)对展品有不同的操作(普通游客:观看讲解;专家:研究细节;摄影师:拍照记录)。展品的结构(绘画、雕塑)不变,但可以新增不同的参观者(新操作),而不需要修改展品的代码。
3. 超简化代码示例(前端场景:对不同类型数据的不同操作)
// 数据结构元素1:用户数据
class UserData {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 接受访问者的访问
accept(visitor) {
visitor.visitUserData(this);
}
}
// 数据结构元素2:商品数据
class ProductData {
constructor(name, price) {
this.name = name;
this.price = price;
}
// 接受访问者的访问
accept(visitor) {
visitor.visitProductData(this);
}
}
// 访问者1:导出数据为JSON格式(操作1)
class JsonExportVisitor {
visitUserData(user) {
return JSON.stringify({ type: 'user', data: user });
}
visitProductData(product) {
return JSON.stringify({ type: 'product', data: product });
}
}
// 访问者2:导出数据为CSV格式(操作2,新增操作无需修改数据结构)
class CsvExportVisitor {
visitUserData(user) {
return `user,${user.name},${user.age}`;
}
visitProductData(product) {
return `product,${product.name},${product.price}`;
}
}
// 调用者:使用不同访问者操作数据
const user = new UserData('张三', 20);
const product = new ProductData('手机', 5999);
// 1. 导出为JSON
const jsonVisitor = new JsonExportVisitor();
console.log(user.accept(jsonVisitor)); // {"type":"user","data":{"name":"张三","age":20}}
console.log(product.accept(jsonVisitor)); // {"type":"product","data":{"name":"手机","price":5999}}
// 2. 导出为CSV(新增操作,未修改UserData和ProductData)
const csvVisitor = new CsvExportVisitor();
console.log(user.accept(csvVisitor)); // user,张三,20
console.log(product.accept(csvVisitor)); // product,手机,5999
4. 图解思路
用流程图展示访问者模式的分离逻辑,核心是“数据结构和操作分开,新增操作不用改数据”:
解读:UserData和ProductData是数据结构,它们只负责存储数据,不负责处理数据的操作(比如导出)。导出JSON、CSV等操作都封装在访问者里,数据结构通过accept方法接受访问者访问,访问者会调用对应方法处理数据。想新增导出Excel的操作,只需新建一个访问者类,不用修改UserData和ProductData,相当于“博物馆展品(数据)不负责讲解,不同参观者(访问者)有不同的讲解方式,新增参观者不用改展品”。
graph TD
A[UserData(数据元素)] -- 接受访问 --> B[访问者(JsonExport/CsvExport)]
C[ProductData(数据元素)] -- 接受访问 --> B
B -- 调用对应visit方法 --> D[visitUserData处理用户数据]
B -- 调用对应visit方法 --> E[visitProductData处理商品数据]
D -- 执行操作(导出JSON/CSV) --> F[返回结果]
E -- 执行操作(导出JSON/CSV) --> F
G[新增操作(如导出Excel)] -- 新建访问者类 --> B
十一、9种前端设计模式优缺点对比表格
| 设计模式 | 优点 | 缺点 | 前端适用场景 |
|---|---|---|---|
| 外观模式 | 1. 简化接口,降低使用成本;2. 封装复杂性,隔离子系统变化;3. 减少调用者与子系统的耦合 | 1. 外观类可能变得过于庞大,承担过多职责;2. 新增子系统功能时,可能需要修改外观类 | 封装复杂DOM操作、整合多个API接口、组件库的统一入口 |
| 代理模式 | 1. 控制访问,灵活添加额外功能(缓存、权限、日志);2. 不修改原对象,符合开闭原则;3. 解耦调用者与原对象 | 1. 增加了代理层,可能导致请求处理速度变慢;2. 代码结构变得更复杂(多了代理类) | 图片懒加载、接口请求缓存、权限控制、日志埋点 |
| 工厂模式 | 1. 封装对象创建过程,降低耦合;2. 便于统一管理对象创建;3. 新增产品时,只需新增产品类和修改工厂,符合开闭原则 | 1. 增加了代码复杂度(多了工厂类和产品类);2. 简单场景下显得冗余 | 组件库创建不同类型组件、表单元素生成、路由配置生成 |
| 单例模式 | 1. 保证唯一实例,节省资源;2. 提供全局访问点,方便使用;3. 避免重复创建对象导致的状态不一致 | 1. 单例类职责过重,违背单一职责原则;2. 多线程环境下需要额外处理线程安全问题;3. 测试困难(依赖全局状态) | 全局弹窗、缓存对象、全局状态管理(如Vuex的Store)、浏览器window对象 |
| 策略模式 | 1. 消除大量if-else/switch-case;2. 策略可动态切换,灵活性高;3. 新增策略只需新增类,符合开闭原则 | 1. 策略类数量可能过多;2. 调用者需要了解所有策略的差异,增加使用成本 | 表单验证、动态排序、主题切换、支付方式选择 |
| 迭代器模式 | 1. 统一遍历接口,屏蔽不同聚合对象的差异;2. 简化遍历逻辑,便于代码复用;3. 支持多种遍历方式(正序、倒序) | 1. 简单遍历场景下显得冗余;2. 增加了代码复杂度(多了迭代器类) | 遍历不同数据结构(数组、对象、Set、Map)、表格数据遍历、列表渲染 |
| 观察者模式 | 1. 解耦发布者和订阅者;2. 支持一对多通信,便于状态同步;3. 订阅者可动态添加/移除,灵活性高 | 1. 订阅者过多时,通知效率低;2. 发布者与订阅者间存在隐式依赖,调试困难;3. 可能导致循环依赖 | 全局状态通知、事件总线(EventBus)、组件间通信、消息推送 |
| 中介者模式 | 1. 解耦多个对象间的直接交互;2. 集中管理对象间的通信逻辑,便于维护;3. 简化对象间的关系 | 1. 中介者类可能变得过于复杂,成为“万能类”;2. 中介者故障会影响所有对象 | 多组件间复杂通信、页面状态协调、游戏场景中角色间交互 |
| 访问者模式 | 1. 分离数据结构和操作,新增操作无需修改数据结构;2. 集中管理不同操作,便于维护;3. 支持对同一数据结构进行多种不同操作 | 1. 数据结构变化时,需要修改所有访问者类;2. 增加了代码复杂度(多了访问者类和accept方法);3. 访问者可能需要访问数据结构的内部状态,破坏封装 | 数据导出(多种格式)、数据校验(多种规则)、报表生成、复杂数据的多维度处理 |
十二、总结
设计模式的核心不是“死记硬背”,而是“理解思想”——通过封装、解耦、抽象,让代码更具可维护性、可扩展性和可复用性。
最后再提一句:没有最好的设计模式,只有最适合的场景。简单场景下无需过度设计,复杂场景下合理运用设计模式,才能真正提升开发效率。
如果这篇文章对你有帮助,别忘了点赞+收藏+关注~ 后续会分享更多前端进阶干货!
(注:文档部分内容可能由 AI 生成)