常用编程模式详解
在入门篇中我们了解了编程模式的概念和价值。本文将深入讲解前端开发中最常用的编程模式,每个模式都会从问题场景出发,配合代码示例和反例对比,帮助你真正理解"什么时候用、怎么用"。
一、创建型模式
创建型模式关注的是对象如何被创建,目标是让创建过程更灵活、更可控。
1.1 单例模式(Singleton)
核心思想: 保证一个类只有一个实例,并提供全局访问点。
前端典型场景: 全局状态管理、弹窗管理器、日志服务、WebSocket 连接。
反例:没有单例的问题
// ❌ 每次 new 都会创建新实例,状态不共享
class Logger {
logs: string[] = []
log(msg: string) { this.logs.push(msg) }
}
const logger1 = new Logger()
const logger2 = new Logger()
logger1.log('hello')
console.log(logger2.logs) // [] — 两个实例互不相关
正例:单例实现
// ✅ 方式一:类实现
class Logger {
private static instance: Logger
logs: string[] = []
private constructor() {}
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
log(msg: string) { this.logs.push(msg) }
}
const logger1 = Logger.getInstance()
const logger2 = Logger.getInstance()
logger1.log('hello')
console.log(logger2.logs) // ['hello'] — 同一个实例
console.log(logger1 === logger2) // true
// ✅ 方式二:模块单例(前端最常用,利用 ES Module 天然缓存)
// logger.ts
class Logger {
logs: string[] = []
log(msg: string) { this.logs.push(msg) }
}
export const logger = new Logger()
// 任何文件 import { logger } 拿到的都是同一个实例
前端项目中,ES Module 本身就是单例的——同一个模块只会被执行一次。所以大多数场景直接导出实例即可,不需要写经典的 getInstance 模式。
1.2 工厂模式(Factory)
核心思想: 把对象的创建逻辑集中到一个地方,调用方不需要关心具体创建细节。
前端典型场景: 根据类型创建不同组件、根据配置生成不同 HTTP 客户端、表单控件工厂。
反例:散落的创建逻辑
// ❌ 每个地方都要写判断逻辑,新增类型时到处改
function renderField(type: string) {
if (type === 'input') return { tag: 'input', props: { type: 'text' } }
if (type === 'select') return { tag: 'select', props: {} }
if (type === 'date') return { tag: 'input', props: { type: 'date' } }
return { tag: 'input', props: {} }
}
正例:工厂模式
// ✅ 集中管理创建逻辑,新增类型只需扩展 map
interface FieldConfig {
tag: string
props: Record<string, unknown>
}
const fieldFactory: Record<string, () => FieldConfig> = {
input: () => ({ tag: 'input', props: { type: 'text' } }),
select: () => ({ tag: 'select', props: {} }),
date: () => ({ tag: 'input', props: { type: 'date' } }),
}
function createField(type: string): FieldConfig {
const creator = fieldFactory[type] ?? fieldFactory.input
return creator()
}
// 新增类型只需加一行
fieldFactory.textarea = () => ({ tag: 'textarea', props: { rows: 3 } })
当创建逻辑简单且固定时,直接 new 就好。当创建逻辑涉及条件判断、需要根据配置动态决定时,工厂模式能让代码更清晰。
二、结构型模式
结构型模式关注的是如何组合对象和类,让它们形成更大的结构,同时保持灵活。
2.1 适配器模式(Adapter)
核心思想: 把一个接口转换成调用方期望的另一个接口,解决接口不兼容问题。
前端典型场景: 对接不同后端 API 格式、封装第三方库、兼容新旧数据结构。
实际场景:对接不同格式的用户 API
// 旧接口返回格式
interface OldUser { userName: string; userAge: number }
// 新接口返回格式
interface NewUser { name: string; age: number }
// 统一的内部格式
interface User { name: string; age: number }
// 适配器:把旧格式转成统一格式
function adaptOldUser(old: OldUser): User {
return { name: old.userName, age: old.userAge }
}
// 业务代码只依赖 User 接口,不关心数据源
function renderProfile(user: User) {
console.log(`${user.name}, ${user.age}岁`)
}
2.2 装饰器模式(Decorator)
核心思想: 在不修改原有对象的前提下,动态地给它添加新功能。
前端典型场景: 给函数添加日志、性能计时、权限校验、防抖节流。
示例:函数装饰器
// 通用日志装饰器
function withLog<T extends (...args: any[]) => any>(fn: T): T {
return function (...args: any[]) {
console.log(`调用 ${fn.name},参数:`, args)
const result = fn(...args)
console.log(`结果:`, result)
return result
} as T
}
function add(a: number, b: number) { return a + b }
const loggedAdd = withLog(add)
loggedAdd(1, 2)
// 调用 add,参数: [1, 2]
// 结果: 3
示例:防抖装饰器(高频实用)
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timer: ReturnType<typeof setTimeout>
return function (...args: any[]) {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
} as T
}
const handleSearch = debounce((keyword: string) => {
console.log('搜索:', keyword)
}, 300)
2.3 代理模式(Proxy)
核心思想: 为对象提供一个代理,控制对原对象的访问。
前端典型场景: Vue 3 响应式系统、数据校验、缓存代理、懒加载。
示例:缓存代理
// 用 Proxy 实现接口缓存
function createCachedFetcher() {
const cache = new Map<string, unknown>()
return new Proxy(fetch, {
apply(target, thisArg, [url, ...rest]: [string, ...any[]]) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url))
}
return target(url, ...rest)
.then((res: Response) => res.json())
.then((data: unknown) => {
cache.set(url, data)
return data
})
},
})
}
const cachedFetch = createCachedFetcher()
await cachedFetch('/api/user/1') // 第一次:发请求
await cachedFetch('/api/user/1') // 第二次:走缓存,不发请求
示例:数据校验代理(Vue 3 响应式的简化原理)
const user = new Proxy({ name: '', age: 0 }, {
set(target, key, value) {
if (key === 'age' && (typeof value !== 'number' || value < 0)) {
throw new Error('age 必须是非负数')
}
return Reflect.set(target, key, value)
},
})
user.name = '小明' // ✅
user.age = 18 // ✅
user.age = -1 // ❌ 抛出错误
Vue 3 的响应式系统正是基于 Proxy 实现的。当你写 reactive({ count: 0 }) 时,Vue 内部就是用 Proxy 拦截了 get/set,从而实现依赖收集和触发更新。
三、行为型模式
行为型模式关注的是对象之间如何通信和分配职责。
3.1 观察者模式(Observer)
核心思想: 定义对象间一对多的依赖关系,当一个对象状态变化时,所有依赖它的对象都会收到通知。
前端典型场景: DOM 事件监听、React/Vue 的状态更新机制。
class Subject<T> {
private observers: Array<(data: T) => void> = []
subscribe(fn: (data: T) => void) {
this.observers.push(fn)
// 返回取消订阅函数
return () => {
this.observers = this.observers.filter((f) => f !== fn)
}
}
notify(data: T) {
this.observers.forEach((fn) => fn(data))
}
}
// 使用
const store = new Subject<number>()
const unsub = store.subscribe((val) => console.log('A 收到:', val))
store.subscribe((val) => console.log('B 收到:', val))
store.notify(42)
// A 收到: 42
// B 收到: 42
unsub() // A 取消订阅
store.notify(100)
// B 收到: 100
3.2 发布订阅模式(Pub/Sub)
核心思想: 与观察者模式类似,但通过一个事件中心完全解耦发布者和订阅者。
关键区别: 观察者模式中,Subject 直接持有 Observer 引用;发布订阅中,双方只和事件中心交互,彼此不知道对方存在。
type Handler = (...args: any[]) => void
class EventBus {
private events = new Map<string, Set<Handler>>()
on(event: string, handler: Handler) {
if (!this.events.has(event)) this.events.set(event, new Set())
this.events.get(event)!.add(handler)
return () => this.off(event, handler)
}
// 只触发一次
once(event: string, handler: Handler) {
const wrapper: Handler = (...args) => {
handler(...args)
this.off(event, wrapper)
}
this.on(event, wrapper)
}
off(event: string, handler: Handler) {
this.events.get(event)?.delete(handler)
}
emit(event: string, ...args: any[]) {
this.events.get(event)?.forEach((fn) => fn(...args))
}
}
// 使用
const bus = new EventBus()
bus.on('login', (user: string) => console.log(`欢迎 ${user}`))
bus.once('login', (user: string) => console.log('首次登录奖励!'))
bus.emit('login', '小明')
// 欢迎 小明
// 首次登录奖励!
bus.emit('login', '小明')
// 欢迎 小明(once 的回调不再触发)
- 组件内部状态变化通知 → 观察者模式(直接、简单)
- 跨模块/跨组件通信 → 发布订阅(解耦更彻底)
- 实际项目中,Vue 的
watch、React 的useEffect都是观察者思想;而mitt、EventEmitter属于发布订阅
3.3 策略模式(Strategy)
核心思想: 定义一系列算法,把它们封装起来,使它们可以互相替换。
前端典型场景: 表单校验规则、价格计算、权限判断、排序策略。
反例:if/else 地狱
// ❌ 每次新增校验规则都要改这个函数
function validate(value: string, rule: string): string | null {
if (rule === 'required') {
if (!value.trim()) return '不能为空'
} else if (rule === 'email') {
if (!/\S+@\S+\.\S+/.test(value)) return '邮箱格式不正确'
} else if (rule === 'minLength') {
if (value.length < 6) return '长度不能少于 6'
}
// 越来越长...
return null
}
正例:策略模式
// ✅ 每个规则独立,新增规则只需扩展
type Validator = (value: string) => string | null
const validators: Record<string, Validator> = {
required: (v) => (!v.trim() ? '不能为空' : null),
email: (v) => (!/\S+@\S+\.\S+/.test(v) ? '邮箱格式不正确' : null),
minLength: (v) => (v.length < 6 ? '长度不能少于 6' : null),
}
// 新增规则:一行搞定
validators.phone = (v) => (!/^1\d{10}$/.test(v) ? '手机号格式不正确' : null)
// 执行校验
function validate(value: string, rules: string[]): string[] {
return rules
.map((rule) => validators[rule]?.(value))
.filter((msg): msg is string => msg !== null)
}
validate('', ['required', 'email'])
// ['不能为空', '邮箱格式不正确']
3.4 责任链模式(Chain of Responsibility)
核心思想: 将请求沿着处理链传递,每个处理者决定自己是否处理,或者传给下一个。
前端典型场景: 中间件(Express/Koa)、拦截器(Axios)、多级审批流程。
type Middleware = (ctx: Record<string, any>, next: () => void) => void
function compose(middlewares: Middleware[]) {
return (ctx: Record<string, any>) => {
let index = 0
function next() {
if (index < middlewares.length) {
middlewares[index++](ctx, next)
}
}
next()
}
}
// 使用:模拟请求处理管道
const authMiddleware: Middleware = (ctx, next) => {
console.log('1. 检查登录状态')
ctx.user = '小明'
next()
}
const logMiddleware: Middleware = (ctx, next) => {
console.log('2. 记录日志:', ctx.user)
next()
}
const handler: Middleware = (ctx) => {
console.log('3. 处理业务:', ctx.user)
}
const pipeline = compose([authMiddleware, logMiddleware, handler])
pipeline({})
// 1. 检查登录状态
// 2. 记录日志: 小明
// 3. 处理业务: 小明
你常用的 axios.interceptors.request.use() 和 axios.interceptors.response.use() 就是典型的责任链模式——请求和响应依次经过每个拦截器处理。
3.5 迭代器模式(Iterator)
核心思想: 提供一种统一的方式来遍历集合,而不暴露集合的内部结构。
前端典型场景: for...of、展开运算符 ...、自定义数据结构遍历。
JavaScript 内置了迭代器协议(Symbol.iterator),数组、Map、Set 都实现了它。我们也可以给自定义对象实现迭代器:
// 自定义范围迭代器
class Range {
constructor(private start: number, private end: number) {}
[Symbol.iterator]() {
let current = this.start
const end = this.end
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true }
},
}
}
}
// 使用
for (const n of new Range(1, 5)) {
console.log(n) // 1, 2, 3, 4, 5
}
console.log([...new Range(3, 6)]) // [3, 4, 5, 6]
实用场景:分页数据迭代器
async function* fetchPages(url: string) {
let page = 1
while (true) {
const res = await fetch(`${url}?page=${page}`)
const data = await res.json()
if (data.items.length === 0) return
yield data.items
page++
}
}
// 使用:逐页处理,不需要一次加载全部
for await (const items of fetchPages('/api/products')) {
console.log('本页数据:', items)
}
四、模式对比与选型指南
| 模式 | 解决什么问题 | 典型前端场景 | 复杂度 |
|---|---|---|---|
| 单例 | 全局唯一实例 | 状态管理、日志、WebSocket | ⭐ |
| 工厂 | 创建逻辑集中管理 | 动态组件、表单控件 | ⭐ |
| 适配器 | 接口不兼容 | API 格式转换、第三方库封装 | ⭐ |
| 装饰器 | 不改原代码加功能 | 日志、防抖、权限 | ⭐⭐ |
| 代理 | 控制访问 | 缓存、校验、Vue 响应式 | ⭐⭐ |
| 观察者 | 状态变化通知 | DOM 事件、watch/useEffect | ⭐⭐ |
| 发布订阅 | 跨模块解耦通信 | EventBus、消息系统 | ⭐⭐ |
| 策略 | 算法/规则可替换 | 表单校验、价格计算 | ⭐ |
| 责任链 | 请求链式处理 | 中间件、拦截器 | ⭐⭐ |
| 迭代器 | 统一遍历方式 | for...of、分页加载 | ⭐ |
五、实践任务
任务一:表单校验器
用策略模式实现一个表单校验器,支持以下规则:
required:必填email:邮箱格式minLength(n):最小长度- 能方便地新增自定义规则
任务二:简易中间件系统
实现一个 compose 函数,支持:
- 按顺序执行中间件
- 每个中间件可以决定是否调用
next() - 用它实现一个"请求日志 → 权限校验 → 业务处理"的管道
任务三:响应式数据
用 Proxy 实现一个简易的 reactive 函数:
- 拦截属性读写
- 属性变化时自动打印日志
- 思考:Vue 3 的
reactive还做了哪些额外工作?
面试高频问答
1) 观察者模式和发布订阅模式的区别?
观察者模式中,Subject 直接持有 Observer 的引用,耦合度较高。发布订阅模式通过事件中心解耦,发布者和订阅者互不知道对方存在。前端中 addEventListener 更接近观察者,EventEmitter / mitt 更接近发布订阅。
2) 策略模式的核心优势是什么?
消除大量 if/else 或 switch,将每种算法/规则独立封装。新增策略时只需扩展,不修改已有代码(开闭原则)。测试时也可以单独测试每个策略。
3) 前端哪些地方用到了代理模式?
- Vue 3 的
reactive()基于Proxy实现响应式 - ES6
Proxy可用于数据校验、属性访问控制、日志记录 - Axios 拦截器本质上也是代理思想
4) 装饰器模式和中间件有什么关系?
装饰器是对单个函数/对象的增强(如加日志、加缓存);中间件是责任链模式的应用,多个处理函数按顺序组成管道。两者都是"不改原有逻辑,在外层包一层"的思想,但组织方式不同。
5) 什么时候不该用设计模式?
- 业务简单、逻辑固定、不会扩展时
- 模式引入后代码量和理解成本明显增加,但收益很小时
- 团队成员不熟悉该模式,会增加协作成本时
记住:模式是工具,不是目标。先写出能工作的代码,当你感受到"痛点"时再引入模式重构。