设计模式
观察者模式(Observer)
概念
观察者模式定义了对象间一对多的依赖关系:当一个对象(Subject,被观察者)的状态发生变化时,所有依赖它的对象(Observer,观察者)都会被自动通知并更新。
UML 关系
┌─────────────────────┐ ┌──────────────────────┐
│ Subject │ │ Observer │
├─────────────────────┤ ├──────────────────────┤
│ - observers: [] │ 1 * │ │
│ │────────▶│ + update(data): void │
│ + attach(observer) │ │ │
│ + detach(observer) │ └──────────────────────┘
│ + notify() │ ▲
└─────────────────────┘ │
┌──────────┴───────────┐
│ ConcreteObserver │
├──────────────────────┤
│ + update(data): void │
└──────────────────────┘Subject 直接持有 Observer 的引用列表,状态变化时遍历列表调用每个 Observer 的 update 方法。观察者和被观察者之间是直接依赖关系。
代码实现
class Subject {
constructor() {
this.observers = []
}
attach(observer) {
if (!this.observers.includes(observer)) {
this.observers.push(observer)
}
}
detach(observer) {
this.observers = this.observers.filter(o => o !== observer)
}
notify(data) {
this.observers.forEach(observer => observer.update(data))
}
}
class Observer {
constructor(name) {
this.name = name
}
update(data) {
console.log(`${this.name} received: ${JSON.stringify(data)}`)
}
}
const subject = new Subject()
const obs1 = new Observer('Observer-1')
const obs2 = new Observer('Observer-2')
subject.attach(obs1)
subject.attach(obs2)
subject.notify({ type: 'update', value: 42 })
subject.detach(obs1)
subject.notify({ type: 'update', value: 100 })与发布订阅模式的区别
| 维度 | 观察者模式 | 发布订阅模式 |
|---|---|---|
| 耦合度 | 主题与观察者直接耦合 | 发布者与订阅者完全解耦 |
| 中间层 | 无 | 有事件中心(Broker/EventBus) |
| 通信方式 | 主题直接调用观察者方法 | 通过事件通道间接通信 |
| 事件类型 | 通常为单一事件 | 支持多种命名事件 |
| 典型实现 | DOM EventTarget | Node.js EventEmitter |
核心区别在于:观察者模式是面对面通信,发布订阅模式有一个中间调度中心。观察者模式中 Subject 需要知道 Observer 的存在(持有引用),而发布订阅模式中发布者和订阅者互不知晓,只通过事件名关联。
前端实际应用
MutationObserver:观察 DOM 变化IntersectionObserver:观察元素可见性ResizeObserver:观察元素尺寸变化- Vue 2 响应式系统的
Dep(Subject)与Watcher(Observer) - RxJS 的
Observable/Observer
发布订阅模式(Pub/Sub)
概念
发布订阅模式在观察者模式的基础上引入了一个事件中心(Event Channel),发布者通过事件中心发布消息,订阅者通过事件中心订阅消息。发布者和订阅者无需互相知晓,实现了完全解耦。
UML 关系
┌───────────┐ ┌──────────────────────┐ ┌────────────┐
│ Publisher │────▶│ EventEmitter │◀────│ Subscriber │
│ │ ├──────────────────────┤ │ │
│ emit(event│ │ - events: Map │ │ on(event, │
│ , data) │ │ │ │ callback)│
└───────────┘ │ + on(event, fn) │ └────────────┘
│ + off(event, fn) │
│ + once(event, fn) │
│ + emit(event, ...args│
└──────────────────────┘手写 EventEmitter 完整实现
class EventEmitter {
constructor() {
this._events = new Map()
}
on(event, fn) {
const fns = this._events.get(event) || []
fns.push(fn)
this._events.set(event, fns)
return this
}
off(event, fn) {
if (!fn) {
this._events.delete(event)
return this
}
const fns = this._events.get(event)
if (!fns) return this
this._events.set(
event,
fns.filter(f => f !== fn && f._original !== fn)
)
return this
}
once(event, fn) {
const wrapper = (...args) => {
fn.apply(this, args)
this.off(event, wrapper)
}
wrapper._original = fn
this.on(event, wrapper)
return this
}
emit(event, ...args) {
const fns = this._events.get(event)
if (!fns || fns.length === 0) return false
fns.slice().forEach(fn => fn.apply(this, args))
return true
}
listenerCount(event) {
const fns = this._events.get(event)
return fns ? fns.length : 0
}
removeAllListeners(event) {
if (event) {
this._events.delete(event)
} else {
this._events.clear()
}
return this
}
}实现细节要点:
once通过包装函数实现,执行后自动解绑。wrapper._original = fn是为了让off在取消once注册的事件时能正确匹配原始函数emit中使用fns.slice()创建副本后遍历,避免在回调中调用off导致数组在遍历时被修改- 链式调用:
on/off/once均返回this
前端实际应用
- Node.js
events模块 - Vue 2 的
$on/$off/$emit(组件事件系统) - 微前端通信(qiankun
initGlobalState) - WebSocket 消息分发
- 跨组件通信 EventBus
策略模式(Strategy)
概念
策略模式定义一系列算法,将每个算法封装为独立策略对象,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。
UML 关系
┌──────────────┐ ┌─────────────────────┐
│ Context │ │ Strategy │
├──────────────┤ ├─────────────────────┤
│ - strategy │──────▶│ + execute(): result │
│ │ └─────────────────────┘
│ + setStrategy│ ▲
│ + execute() │ ┌────────┼────────┐
└──────────────┘ │ │ │
StrategyA StrategyB StrategyC代码实现:表单验证场景
const validators = {
required(value) {
return value !== '' && value !== null && value !== undefined
? { valid: true }
: { valid: false, message: '此字段为必填项' }
},
minLength(value, min) {
return value.length >= min
? { valid: true }
: { valid: false, message: `最少需要 ${min} 个字符` }
},
maxLength(value, max) {
return value.length <= max
? { valid: true }
: { valid: false, message: `最多允许 ${max} 个字符` }
},
pattern(value, regex) {
return regex.test(value)
? { valid: true }
: { valid: false, message: '格式不正确' }
},
email(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? { valid: true }
: { valid: false, message: '请输入有效的邮箱地址' }
},
phone(value) {
return /^1[3-9]\d{9}$/.test(value)
? { valid: true }
: { valid: false, message: '请输入有效的手机号码' }
}
}
class FormValidator {
constructor() {
this.rules = new Map()
}
addRule(field, ruleConfigs) {
this.rules.set(field, ruleConfigs)
return this
}
validate(formData) {
const errors = {}
let isValid = true
for (const [field, ruleConfigs] of this.rules) {
const value = formData[field]
for (const config of ruleConfigs) {
const { strategy, ...params } = config
const validator = validators[strategy]
const args = [value, ...Object.values(params)]
const result = validator(...args)
if (!result.valid) {
errors[field] = config.message || result.message
isValid = false
break
}
}
}
return { isValid, errors }
}
}
const validator = new FormValidator()
validator
.addRule('username', [
{ strategy: 'required' },
{ strategy: 'minLength', min: 3 },
{ strategy: 'maxLength', max: 20 }
])
.addRule('email', [
{ strategy: 'required' },
{ strategy: 'email' }
])
.addRule('phone', [
{ strategy: 'required' },
{ strategy: 'phone' }
])
const result = validator.validate({
username: 'ab',
email: 'invalid',
phone: '12345'
})前端实际应用
- 表单验证策略(如 async-validator、VeeValidate)
- 权限控制策略(不同角色不同操作集)
- 动画缓动函数(linear、ease-in、ease-out 作为策略)
- 数据格式化(日期、货币、数字的多种格式化策略)
- 日志上报策略(不同环境使用不同上报方式)
工厂模式(Factory)
概念
工厂模式将对象的创建逻辑封装起来,调用方无需知道具体的创建细节。根据抽象程度的不同,分为三种变体。
简单工厂
由一个工厂函数根据参数创建不同类型的对象:
class Button {
constructor(options) {
this.type = options.type
this.text = options.text
this.size = options.size || 'medium'
}
render() {
return `<button class="btn-${this.type} btn-${this.size}">${this.text}</button>`
}
}
class Input {
constructor(options) {
this.type = options.inputType || 'text'
this.placeholder = options.placeholder
}
render() {
return `<input type="${this.type}" placeholder="${this.placeholder}" />`
}
}
class Select {
constructor(options) {
this.options = options.options || []
this.placeholder = options.placeholder
}
render() {
const opts = this.options.map(o => `<option value="${o.value}">${o.label}</option>`).join('')
return `<select>${opts}</select>`
}
}
function createComponent(type, options) {
const componentMap = {
button: Button,
input: Input,
select: Select
}
const Component = componentMap[type]
if (!Component) {
throw new Error(`Unknown component type: ${type}`)
}
return new Component(options)
}
const btn = createComponent('button', { type: 'primary', text: '提交' })
const input = createComponent('input', { placeholder: '请输入' })工厂方法
将创建对象的职责委托给子类,每个子类负责创建特定类型的产品:
class DialogFactory {
createDialog() {
throw new Error('Subclass must implement createDialog')
}
show(options) {
const dialog = this.createDialog()
dialog.setContent(options.content)
dialog.render()
return dialog
}
}
class ConfirmDialogFactory extends DialogFactory {
createDialog() {
return new ConfirmDialog()
}
}
class AlertDialogFactory extends DialogFactory {
createDialog() {
return new AlertDialog()
}
}
class ConfirmDialog {
setContent(content) {
this.content = content
}
render() {
console.log(`[Confirm] ${this.content} [OK] [Cancel]`)
}
}
class AlertDialog {
setContent(content) {
this.content = content
}
render() {
console.log(`[Alert] ${this.content} [OK]`)
}
}
const confirmFactory = new ConfirmDialogFactory()
confirmFactory.show({ content: '确定要删除吗?' })抽象工厂
提供一个创建一系列相关对象的接口,确保产品族之间的一致性:
class LightThemeFactory {
createButton(text) {
return {
render: () => `<button class="light-btn">${text}</button>`,
theme: 'light'
}
}
createCard(content) {
return {
render: () => `<div class="light-card">${content}</div>`,
theme: 'light'
}
}
createInput(placeholder) {
return {
render: () => `<input class="light-input" placeholder="${placeholder}" />`,
theme: 'light'
}
}
}
class DarkThemeFactory {
createButton(text) {
return {
render: () => `<button class="dark-btn">${text}</button>`,
theme: 'dark'
}
}
createCard(content) {
return {
render: () => `<div class="dark-card">${content}</div>`,
theme: 'dark'
}
}
createInput(placeholder) {
return {
render: () => `<input class="dark-input" placeholder="${placeholder}" />`,
theme: 'dark'
}
}
}
function createUI(factory) {
const button = factory.createButton('Submit')
const card = factory.createCard('Hello World')
const input = factory.createInput('Type here...')
return { button, card, input }
}
const lightUI = createUI(new LightThemeFactory())
const darkUI = createUI(new DarkThemeFactory())UML 关系(抽象工厂)
┌─────────────────────┐
│ AbstractFactory │
├─────────────────────┤
│ + createButton() │
│ + createCard() │
│ + createInput() │
└─────────┬───────────┘
│
┌─────┴──────┐
│ │
┌───┴────┐ ┌───┴────┐
│ Light │ │ Dark │ ← 每个工厂生产同一主题的完整产品族
│ Factory│ │ Factory│
└────────┘ └────────┘前端实际应用
- React
createElement就是一个简单工厂 - 组件库的主题系统(Ant Design 的 ConfigProvider)
- 跨平台 UI 抽象(React Native、Taro 根据平台产出不同组件)
- 日志系统(根据环境创建不同的 Logger 实例)
单例模式(Singleton)
概念
单例模式确保一个类只有一个实例,并提供一个全局访问点。在前端中常用于管理全局状态、弹窗、缓存等共享资源。
UML 关系
┌────────────────────────┐
│ Singleton │
├────────────────────────┤
│ - static instance │
├────────────────────────┤
│ - constructor() │ ← 私有构造
│ + static getInstance() │ ← 全局访问点
│ + operation() │
└────────────────────────┘懒汉式实现
实例在首次访问时才创建:
class Singleton {
static instance = null
static getInstance(...args) {
if (!Singleton.instance) {
Singleton.instance = new Singleton(...args)
}
return Singleton.instance
}
constructor(config) {
if (Singleton.instance) {
return Singleton.instance
}
this.config = config
Singleton.instance = this
}
}
const a = Singleton.getInstance({ theme: 'dark' })
const b = Singleton.getInstance({ theme: 'light' })
a === b
a.config.theme代理实现
利用闭包和代理分离单例管理逻辑与业务逻辑,实现更通用的单例方案:
function createSingleton(ClassRef) {
let instance = null
return new Proxy(ClassRef, {
construct(target, args) {
if (!instance) {
instance = Reflect.construct(target, args)
}
return instance
}
})
}
class Store {
constructor(initialState) {
this.state = initialState
}
getState() {
return this.state
}
setState(partial) {
this.state = { ...this.state, ...partial }
}
}
const SingletonStore = createSingleton(Store)
const store1 = new SingletonStore({ count: 0 })
const store2 = new SingletonStore({ count: 100 })
store1 === store2
store1.getState()也可以用高阶函数实现更函数式的版本:
function singleton(fn) {
let result
return function (...args) {
if (!result) {
result = fn.apply(this, args)
}
return result
}
}
const createStore = singleton((initialState) => {
let state = initialState
return {
getState: () => state,
setState: (partial) => { state = { ...state, ...partial } }
}
})应用场景
全局模态框管理:
class ModalManager {
static instance = null
static getInstance() {
if (!ModalManager.instance) {
ModalManager.instance = new ModalManager()
}
return ModalManager.instance
}
constructor() {
this.modals = new Map()
this.activeModal = null
}
register(id, modal) {
this.modals.set(id, modal)
}
open(id, props) {
if (this.activeModal) {
this.close(this.activeModal)
}
const modal = this.modals.get(id)
if (modal) {
modal.open(props)
this.activeModal = id
}
}
close(id) {
const modal = this.modals.get(id || this.activeModal)
if (modal) {
modal.close()
this.activeModal = null
}
}
}前端实际应用
- Vuex/Pinia/Redux Store(全局唯一状态树)
- 全局 Loading/Toast 管理器
- WebSocket 连接管理
- 浏览器
window对象本身就是单例 - 日志收集器(全局唯一 Logger 实例)
代理模式(Proxy)
概念
代理模式为目标对象提供一个代理对象,由代理对象控制对目标对象的访问。代理可以在不改变目标对象接口的前提下,对访问进行拦截和增强。
UML 关系
┌──────────────┐ ┌──────────────────────┐
│ Client │──────▶│ Subject │
└──────────────┘ ├──────────────────────┤
│ + request() │
└──────────┬───────────┘
│
┌───────────┴──────────┐
│ │
┌──────┴──────┐ ┌────────┴────────┐
│ Proxy │─────▶│ RealSubject │
├─────────────┤ ├─────────────────┤
│ + request() │ │ + request() │
└─────────────┘ └─────────────────┘与 ES6 Proxy 的关系
ES6 Proxy 是语言层面对代理模式的原生实现。它提供了一种机制,可以拦截并自定义对象的基本操作(属性读取、赋值、枚举、函数调用等):
const handler = {
get(target, prop, receiver) {
console.log(`Reading property: ${String(prop)}`)
return Reflect.get(target, prop, receiver)
},
set(target, prop, value, receiver) {
console.log(`Setting ${String(prop)} = ${value}`)
return Reflect.set(target, prop, value, receiver)
},
deleteProperty(target, prop) {
console.log(`Deleting property: ${String(prop)}`)
return Reflect.deleteProperty(target, prop)
}
}
const obj = new Proxy({}, handler)
obj.name = 'Alice'
obj.name
delete obj.nameVue 3 的响应式系统正是基于 Proxy 实现的,通过拦截 get 收集依赖、拦截 set 触发更新。
缓存代理
function createCacheProxy(fn) {
const cache = new Map()
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
console.log(`Cache hit for: ${key}`)
return cache.get(key)
}
const result = Reflect.apply(target, thisArg, args)
if (result instanceof Promise) {
return result.then(value => {
cache.set(key, value)
return value
})
}
cache.set(key, result)
return result
}
})
}
function fibonacci(n) {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
const cachedFib = createCacheProxy(fibonacci)
cachedFib(40)
cachedFib(40)
const fetchUser = createCacheProxy(async (id) => {
const res = await fetch(`/api/users/${id}`)
return res.json()
})虚拟代理
延迟创建开销大的对象,直到真正需要时才初始化:
class HeavyEditor {
constructor() {
this.loaded = false
}
async init() {
console.log('Loading editor resources...')
await new Promise(resolve => setTimeout(resolve, 2000))
this.loaded = true
console.log('Editor ready')
}
setContent(content) {
this.content = content
}
getContent() {
return this.content
}
}
class EditorProxy {
constructor() {
this.editor = null
this.pendingOps = []
}
async ensureEditor() {
if (!this.editor) {
this.editor = new HeavyEditor()
await this.editor.init()
for (const op of this.pendingOps) {
op(this.editor)
}
this.pendingOps = []
}
return this.editor
}
setContent(content) {
if (this.editor) {
this.editor.setContent(content)
} else {
this.pendingOps.push(editor => editor.setContent(content))
}
}
async getContent() {
const editor = await this.ensureEditor()
return editor.getContent()
}
}
const editor = new EditorProxy()
editor.setContent('Hello World')
editor.getContent().then(console.log)前端实际应用
- Vue 3
reactive/ref响应式代理 - 图片懒加载(虚拟代理:先加载占位图,可视时加载真实图片)
- API 请求缓存代理
- 属性验证代理(限制对象属性的类型和范围)
Proxy实现不可变数据(拦截set抛出错误)
装饰器模式(Decorator)
概念
装饰器模式在不修改原始对象的前提下,动态地为对象添加额外的职责。与继承相比,装饰器更加灵活——可以在运行时组合多个装饰器,形成功能叠加。
UML 关系
┌──────────────────┐
│ Component │
├──────────────────┤
│ + operation() │
└────────┬─────────┘
│
┌────┴──────────────┐
│ │
┌───┴──────────┐ ┌─────┴──────────┐
│ Concrete │ │ Decorator │
│ Component │ ├────────────────┤
│ │ │ - component │──▶ Component
└──────────────┘ │ + operation() │
└────────┬───────┘
│
┌────────┴───────┐
│ConcreteDecorator│
│ + operation() │
└────────────────┘AOP(面向切面编程)实现
Function.prototype.before = function (beforeFn) {
const self = this
return function (...args) {
beforeFn.apply(this, args)
return self.apply(this, args)
}
}
Function.prototype.after = function (afterFn) {
const self = this
return function (...args) {
const result = self.apply(this, args)
afterFn.apply(this, args)
return result
}
}
function submitForm(data) {
console.log('Submitting:', data)
return { success: true }
}
const enhanced = submitForm
.before((data) => {
console.log('Validating:', data)
})
.after((data) => {
console.log('Logging submission:', data)
})
enhanced({ name: 'Alice' })不污染原型的纯函数版本:
function before(fn, beforeFn) {
return function (...args) {
beforeFn.apply(this, args)
return fn.apply(this, args)
}
}
function after(fn, afterFn) {
return function (...args) {
const result = fn.apply(this, args)
afterFn.apply(this, args)
return result
}
}
function compose(...decorators) {
return function (fn) {
return decorators.reduceRight((decorated, decorator) => decorator(decorated), fn)
}
}
const withLogging = (fn) => before(fn, (...args) => console.log('Call:', args))
const withTiming = (fn) => {
return function (...args) {
const start = performance.now()
const result = fn.apply(this, args)
console.log(`Duration: ${performance.now() - start}ms`)
return result
}
}
const enhance = compose(withLogging, withTiming)
const enhancedFetch = enhance(fetch)TypeScript 装饰器语法
TypeScript 5.0+ 支持 TC39 Stage 3 的标准装饰器语法:
function log(originalMethod: Function, context: ClassMethodDecoratorContext) {
return function (this: any, ...args: any[]) {
console.log(`Calling ${String(context.name)} with`, args)
const result = originalMethod.apply(this, args)
console.log(`Result:`, result)
return result
}
}
function readonly(
_target: undefined,
context: ClassFieldDecoratorContext
) {
return function (this: any, initialValue: any) {
return initialValue
}
}
function sealed(constructor: Function, _context: ClassDecoratorContext) {
Object.seal(constructor)
Object.seal(constructor.prototype)
}
@sealed
class UserService {
@log
findUser(id: number) {
return { id, name: 'Alice' }
}
@log
deleteUser(id: number) {
return { success: true }
}
}旧版实验性装饰器(experimentalDecorators)在 NestJS、Angular 等框架中广泛使用:
function Controller(prefix: string) {
return function (target: Function) {
Reflect.defineMetadata('prefix', prefix, target)
}
}
function Get(path: string) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('method', 'GET', target, propertyKey)
Reflect.defineMetadata('path', path, target, propertyKey)
}
}前端实际应用
- React 高阶组件(HOC)本质是装饰器模式
- NestJS 大量使用装饰器(
@Controller、@Get、@Injectable) - Angular 装饰器(
@Component、@Directive、@Pipe) - Mobx
@observable、@action - API 请求增强(添加 token、日志、错误处理)
适配器模式(Adapter)
概念
适配器模式将一个类的接口转换成客户端期望的另一个接口,使原本因接口不兼容而无法协作的类可以一起工作。在前端中,适配器常用于统一不同数据源的接口或兼容新旧 API。
UML 关系
┌──────────┐ ┌──────────────────┐ ┌───────────────┐
│ Client │────▶│ Target │ │ Adaptee │
│ │ ├──────────────────┤ ├───────────────┤
└──────────┘ │ + request() │ │ + legacyReq() │
└────────┬─────────┘ └───────┬───────┘
│ │
┌────────┴─────────┐ │
│ Adapter │─────────────┘
├──────────────────┤ uses
│ + request() │
└──────────────────┘代码实现
统一多个 HTTP 库的接口:
class FetchAdapter {
async request(config) {
const { url, method = 'GET', headers = {}, data } = config
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...headers
},
body: data ? JSON.stringify(data) : undefined
})
const responseData = await response.json()
return {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
data: responseData
}
}
}
class XHRAdapter {
request(config) {
return new Promise((resolve, reject) => {
const { url, method = 'GET', headers = {}, data } = config
const xhr = new XMLHttpRequest()
xhr.open(method, url)
Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value)
})
xhr.onload = () => {
resolve({
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
data: JSON.parse(xhr.responseText)
})
}
xhr.onerror = () => reject(new Error('Network Error'))
xhr.send(data ? JSON.stringify(data) : null)
})
}
}
class HttpClient {
constructor(adapter) {
this.adapter = adapter
}
get(url, config = {}) {
return this.adapter.request({ ...config, url, method: 'GET' })
}
post(url, data, config = {}) {
return this.adapter.request({ ...config, url, method: 'POST', data })
}
}
const client = new HttpClient(new FetchAdapter())
client.get('/api/users')后端接口数据适配:
function adaptUserData(backendUser) {
return {
id: backendUser.user_id,
name: `${backendUser.first_name} ${backendUser.last_name}`,
email: backendUser.email_address,
avatar: backendUser.avatar_url || '/default-avatar.png',
createdAt: new Date(backendUser.create_time * 1000),
role: backendUser.role_type === 1 ? 'admin' : 'user'
}
}
function adaptPaginatedResponse(backendResponse) {
return {
items: backendResponse.data.list.map(adaptUserData),
total: backendResponse.data.total_count,
page: backendResponse.data.page_no,
pageSize: backendResponse.data.page_size,
hasMore: backendResponse.data.page_no * backendResponse.data.page_size < backendResponse.data.total_count
}
}前端实际应用
- Axios 的适配器机制(浏览器用 XMLHttpRequest,Node.js 用 http 模块)
- 地图 SDK 适配(统一高德/百度/Google Maps 的调用接口)
- 后端接口数据转换层(DTO → VO)
- localStorage/sessionStorage/IndexedDB 统一存储接口
- 旧版 jQuery 代码迁移到现代框架时的兼容层