在前端开发中,设计模式是解决重复问题的 "通用模板"------ 它不直接提供可运行的代码,却能指导我们写出更易维护、可扩展的逻辑。尤其是在大型项目(如组件库开发、状态管理、工具函数封装)中,合理运用设计模式能让代码结构更清晰,协作效率翻倍。
今天就带大家手撕前端最常用的 7 种设计模式,每种模式都从 "核心思想→实际应用场景→完整代码实现" 三个维度拆解,结合 Vue、Axios 等大家熟悉的库举例,让抽象的概念落地到真实开发中。
一、工厂模式:批量创建相似对象的 "生产线"
核心思想
工厂模式的本质是用一个函数(或类)封装对象的创建逻辑,外界无需关心对象的具体构造细节,只需调用工厂方法即可获取实例。就像工厂生产产品,你要一个 "手机",工厂直接给你成品,不用管芯片、屏幕怎么组装。
前端应用场景
- Vue3 的
createApp()
:通过createApp(App).mount('#app')
创建 Vue 实例,无需手动 new Vue,内部封装了实例初始化逻辑。 - Axios 的
axios.create()
:基于自定义配置(如 baseURL、超时时间)创建新的 axios 实例,避免全局配置污染。 - 组件库中的 "弹窗工厂":批量创建不同类型的弹窗(警告窗、确认窗、输入窗),统一管理样式和行为。
代码实现:简易 Axios 工厂
javascript
// 模拟Axios工厂:根据配置创建请求实例
class AxiosFactory {
// 静态工厂方法
static create(config = {}) {
// 默认配置
const defaultConfig = {
baseURL: '',
timeout: 5000,
headers: { 'Content-Type': 'application/json' }
};
// 合并用户配置与默认配置
const finalConfig = { ...defaultConfig, ...config };
// 封装请求方法
const request = async (options) => {
const { url, method = 'GET', data = {} } = options;
try {
const response = await fetch(`${finalConfig.baseURL}${url}`, {
method,
headers: finalConfig.headers,
timeout: finalConfig.timeout,
body: method === 'POST' ? JSON.stringify(data) : null
});
return await response.json();
} catch (err) {
console.error('请求失败:', err);
throw err;
}
};
// 返回封装后的请求实例
return {
get: (url, params) => request({ url, params }),
post: (url, data) => request({ url, method: 'POST', data })
};
}
}
// 使用工厂创建实例
const userApi = AxiosFactory.create({ baseURL: 'https://api.user.com' });
const orderApi = AxiosFactory.create({ baseURL: 'https://api.order.com', timeout: 10000 });
// 调用实例方法(无需关心内部实现)
userApi.get('/info');
orderApi.post('/create', { goodsId: 123 });
二、单例模式:确保全局只有一个实例
核心思想
单例模式要求一个类在整个系统中只能有一个实例 ,且提供一个全局访问点。就像浏览器的window
对象、Vue 的Vuex
实例,无论在哪里访问,拿到的都是同一个对象,避免重复创建造成资源浪费。
前端应用场景
- 全局状态管理(Vuex/Pinia):整个应用只有一个 Store 实例,确保状态统一。
- 弹窗组件(如 Vant 的 Toast、Notify):多次调用
Toast('提示')
,不会创建多个弹窗,而是复用同一个并更新内容。 - 全局事件总线(EventBus):避免创建多个总线导致事件监听混乱。
代码实现:ES6 私有属性实现单例
JavaScript
class Singleton {
// 1. 用私有属性(#开头)存储唯一实例,外部无法直接访问
static #instance;
// 2. 私有构造函数:防止外部通过new创建实例
constructor() {
// 若已有实例,直接抛出错误(严格限制单例)
if (Singleton.#instance) {
throw new Error('单例模式不允许重复创建实例,请通过getInstance()获取');
}
}
// 3. 静态方法:全局访问点,确保只创建一个实例
static getInstance() {
// 首次调用时创建实例,后续直接返回已存在的实例
if (!Singleton.#instance) {
Singleton.#instance = new Singleton();
}
return Singleton.#instance;
}
// 示例:单例的业务方法
doSomething() {
console.log('单例实例的业务逻辑');
}
}
// 测试:多次调用获取的是同一个实例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
// 错误用法:外部无法通过new创建实例
// const instance3 = new Singleton(); // 抛出错误
三、观察者模式:"一对多" 的依赖通知机制
核心思想
观察者模式定义了对象间的一对多依赖关系:当 "目标对象" 的状态发生改变时,所有依赖它的 "观察者对象" 会自动收到通知并更新。就像订阅报纸 ------ 你(观察者)订阅了《前端日报》(目标),一旦报纸更新,所有订阅者都会收到新报纸。
前端应用场景
- Vue 的响应式原理:数据(目标)变化时,依赖该数据的 DOM(观察者)自动更新。
- 事件监听(如
addEventListener
):DOM 元素(目标)触发 click 事件时,所有绑定的回调函数(观察者)执行。 - 组件库的自定义事件:父组件监听子组件的
@change
事件,子组件触发时父组件收到通知。
代码实现:简易 EventBus(观察者模式实践)
javascript
class EventBus {
// 私有属性:存储事件与对应的观察者(回调函数)
#handlers = {}; // 结构:{ 事件名: [callback1, callback2, ...] }
// 1. 订阅事件:添加观察者
$on(eventName, callback) {
if (typeof callback !== 'function') {
throw new Error('回调函数必须是function类型');
}
// 若事件不存在,初始化空数组
if (!this.#handlers[eventName]) {
this.#handlers[eventName] = [];
}
// 添加回调到事件列表
this.#handlers[eventName].push(callback);
}
// 2. 触发事件:通知所有观察者
$emit(eventName, ...args) {
// 若事件无订阅者,直接返回
const callbacks = this.#handlers[eventName] || [];
// 执行所有回调,并传递参数
callbacks.forEach(callback => callback(...args));
}
// 3. 取消订阅:移除观察者
$off(eventName) {
if (eventName) {
// 移除指定事件的所有订阅者
this.#handlers[eventName] = [];
} else {
// 若未传事件名,清空所有订阅
this.#handlers = {};
}
}
// 4. 一次性订阅:触发后自动取消订阅
$once(eventName, callback) {
// 封装回调:执行后立即取消订阅
const wrapper = (...args) => {
callback(...args); // 执行原回调
this.$off(eventName); // 取消订阅
};
this.$on(eventName, wrapper);
}
}
// 使用示例
const bus = new EventBus();
// 订阅事件
bus.$on('userLogin', (username) => {
console.log(`欢迎 ${username} 登录`);
});
// 一次性订阅
bus.$once('showTip', (msg) => {
console.log('提示:', msg);
});
// 触发事件
bus.$emit('userLogin', '张三'); // 输出:欢迎 张三 登录
bus.$emit('showTip', '请完善个人资料'); // 输出:提示:请完善个人资料
bus.$emit('showTip', '再次触发'); // 无输出(已取消订阅)
// 取消订阅
bus.$off('userLogin');
bus.$emit('userLogin', '李四'); // 无输出(已取消订阅)
四、发布订阅模式:解耦发布者与订阅者的 "中间件"
核心思想
发布订阅模式是观察者模式的 "升级版",它通过一个 "事件中心" 中间件,让发布者和订阅者完全解耦 ------ 发布者不用知道谁在订阅,订阅者也不用知道谁在发布,两者通过事件中心通信。
举个例子:你(订阅者)在短视频平台订阅了 "前端教程" 标签(事件中心),UP 主(发布者)发布带该标签的视频时,平台(事件中心)会把视频推送给所有订阅该标签的用户。
与观察者模式的区别
维度 | 观察者模式 | 发布订阅模式 |
---|---|---|
耦合度 | 目标与观察者直接依赖 | 发布者、订阅者无直接依赖 |
通信方式 | 目标直接通知观察者 | 通过事件中心间接通信 |
适用场景 | 简单的一对一 / 一对多依赖 | 复杂系统、跨模块通信 |
前端应用场景
- 跨组件通信(如 Vue 的 EventBus、React 的 Context):组件间不直接引用,通过事件中心传递消息。
- 状态管理库(如 Redux):Action(发布者)触发后,Store(事件中心)通知 Reducer 更新状态,组件(订阅者)感知状态变化。
- 浏览器的
CustomEvent
:自定义事件通过dispatchEvent
发布,addEventListener
订阅,浏览器作为事件中心。
五、原型模式:通过 "复制" 创建新对象
核心思想
原型模式是基于已有对象(原型)创建新对象,新对象会继承原型的属性和方法,避免重复定义相似对象。就像工厂生产玩具 ------ 先做一个 "原型玩具",后续所有玩具都复制原型的样式,再微调细节。
在 JavaScript 中,原型模式是语言的核心特性:每个对象都有__proto__
属性,指向它的原型对象,原型链就是基于这个机制实现的。
前端应用场景
Object.create()
:以指定对象为原型,创建新对象(如const obj = Object.create(prototypeObj)
)。- Vue2 的数组方法重写:Vue2 为数组的
push
、pop
等方法添加响应式逻辑,新数组会继承这些重写后的方法。 - 组件复用:Vue 的组件实例会继承组件选项(如
data
、methods
),本质是原型继承。
代码实现:基于原型的对象创建
javascript
// 1. 定义原型对象(被复制的模板)
const userPrototype = {
// 原型方法
greet() {
console.log(`Hello, 我是 ${this.name},年龄 ${this.age}`);
},
// 原型属性
role: 'user'
};
// 2. 基于原型创建新对象(方式1:Object.create)
const user1 = Object.create(userPrototype);
// 为新对象添加自身属性
user1.name = '张三';
user1.age = 20;
user1.greet(); // 输出:Hello, 我是 张三,年龄 20
console.log(user1.role); // 输出:user(继承自原型)
// 3. 基于原型创建新对象(方式2:手动实现原型链)
function createUser(name, age) {
const user = {};
// 设置原型:让新对象继承userPrototype
Object.setPrototypeOf(user, userPrototype);
// 添加自身属性
user.name = name;
user.age = age;
return user;
}
const user2 = createUser('李四', 22);
user2.greet(); // 输出:Hello, 我是 李四,年龄 22
console.log(user2.__proto__ === userPrototype); // true(验证原型链)
六、代理模式:控制对对象的 "访问权限"
核心思想
代理模式为一个对象提供 "代理" 对象,外界通过代理对象访问原对象,代理可以在访问前后添加额外逻辑(如缓存、权限校验、日志记录)。就像你找房产中介(代理)租房,中介会帮你筛选房源、谈判价格,你不用直接和房东(原对象)打交道。
前端应用场景
- 缓存代理:第一次请求数据后缓存结果,重复请求直接返回缓存(如本文开头的英雄查询案例)。
- 权限代理:控制敏感操作的访问权限(如未登录用户无法调用 "提交订单" 接口)。
- Vue3 的响应式代理:通过
Proxy
代理对象,拦截属性的读取 / 修改,实现响应式更新。
代码实现:缓存代理(API 请求优化)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>缓存代理:API请求优化</title>
</head>
<body>
<h1>城市查询(缓存代理演示)</h1>
<input type="text" class="query-input" placeholder="输入省份名查询城市(如:广东省)">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 1. 定义缓存对象:存储已查询的结果
const cache = {};
// 2. 代理函数:封装API请求,添加缓存逻辑
async function searchCity(provinceName) {
// 若缓存中存在,直接返回缓存结果
if (cache[provinceName]) {
console.log('从缓存中获取数据');
return cache[provinceName];
}
// 若缓存中不存在,发起API请求
console.log('从API获取数据');
try {
const response = await axios({
url: 'http://hmajax.itheima.net/api/city',
params: { pname: provinceName }
});
// 缓存结果(键:省份名,值:城市列表)
cache[provinceName] = response.data.list;
return response.data.list;
} catch (err) {
console.error('查询失败:', err);
throw err;
}
}
// 3. 绑定DOM事件:用户输入回车后查询
document.querySelector('.query-input').addEventListener('keyup', async (e) => {
if (e.keyCode === 13) { // 按下回车键
const province = e.target.value.trim();
if (!province) return;
const cities = await searchCity(province);
console.log('查询结果:', cities);
}
});
</script>
</body>
</html>
效果验证:第一次输入 "广东省" 会发起 API 请求,第二次输入 "广东省" 会直接从缓存返回结果,减少网络请求次数。
七、迭代器模式:统一 "遍历" 不同数据结构
核心思想
迭代器模式提供一种统一的遍历接口,让外界可以不关心数据结构的内部实现(如数组、对象、Set、Map),用相同的方式遍历不同数据。就像旅游向导(迭代器)带你游览不同景点(数据结构),你不用知道景点的路线,只需跟着向导走。
在 JavaScript 中,迭代器模式通过 "迭代协议" 实现:
- 可迭代协议 :对象必须有
[Symbol.iterator]
方法,该方法返回一个迭代器对象。 - 迭代器协议 :迭代器对象必须有
next()
方法,每次调用返回{ done: boolean, value: any }
(done
表示是否遍历结束,value
表示当前值)。
前端应用场景
for...of
循环:统一遍历数组、Set、Map、字符串等可迭代对象。- 数组的
forEach
方法:抽象遍历逻辑,用户只需关注每个元素的处理。 - 自定义数据结构遍历:如链表、树结构,通过迭代器实现统一遍历。
代码实现:自定义对象迭代器
javascript
// 定义一个自定义数据结构(包含数组属性)
const customCollection = {
items: ['前端', 'Java', 'Python', 'UI/UX'], // 待遍历的数据
length: 4,
// 1. 实现可迭代协议:添加[Symbol.iterator]方法
[Symbol.iterator]() {
let index = 0; // 遍历索引
const items = this.items; // 保存this指向(避免闭包中this丢失)
// 2. 实现迭代器协议:返回带有next()方法的迭代器对象
return {
next() {
// 遍历未结束:返回当前值和done=false
if (index < items.length) {
return {
done: false,
value: items[index++] // 返回当前值后,索引自增
};
}
// 遍历结束:返回done=true
return { done: true };
}
};
}
// 可选:用Generator简化迭代器实现(更简洁)
// [Symbol.iterator]() {
// function* generator() {
// for (const item of this.items) {
// yield item; // 每次yield返回一个值,自动管理索引
// }
// }
// return generator.call(this);
// }
};
// 3. 用统一方式遍历自定义数据结构(for...of)
for (const item of customCollection) {
console.log('遍历结果:', item); // 依次输出:前端、Java、Python、UI/UX
}
// 4. 手动调用迭代器(验证迭代器协议)
const iterator = customCollection[Symbol.iterator]();
console.log(iterator.next()); // { done: false, value: '前端' }
console.log(iterator.next()); // { done: false, value: 'Java' }
console.log(iterator.next()); // { done: false, value: 'Python' }
console.log(iterator.next()); // { done: false, value: 'UI/UX' }
console.log(iterator.next()); // { done: true }
总结:设计模式不是 "银弹",而是 "工具箱"
最后想强调:设计模式不是必须遵守的 "规则",而是解决问题的 "工具"。在实际开发中,我们不需要刻意追求 "用满所有模式",而是根据场景选择合适的工具:
-
需批量创建对象 → 工厂模式
-
需全局唯一实例 → 单例模式
-
需事件通知 → 观察者 / 发布订阅模式
-
需复用对象 → 原型模式
-
需控制访问 → 代理模式
-
需统一遍历 → 迭代器模式
但设计模式的世界远不止这 6 种,还有很多 "工具" 等待着我们在特定场景下的邂逅,然后通过这些"工具"能帮我们更加优雅地解决问题,这就是设计模式的魅力所在。 记住:好的代码不是 "用了多少设计模式",而是 "是否清晰解决了问题"。希望这篇文章能帮你理解设计模式的本质,让你的代码在 "可维护" 和 "可扩展" 上更上一层楼。
如果您觉得这篇文章对您有帮助,欢迎点赞和收藏,大家的支持是我继续创作优质内容的动力🌹🌹🌹也希望您能在😉😉😉我的主页 😉😉😉找到更多对您有帮助的内容。
- 致敬每一位赶路人