JavaScript设计模式解析:观察者、工厂等5大模式详解
JavaScript 设计模式深度解析:从原理到实践
设计模式是解决特定问题的经验总结,掌握它们能显著提升代码质量和可维护性。本文将深入探讨 JavaScript 中最实用的几种设计模式,包括观察者模式、工厂模式、单例模式、装饰器模式和代理模式。
一、观察者模式 vs 发布-订阅模式
1.1 观察者模式(Observer Pattern)
观察者模式定义了一种一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知。
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log(`Received data: ${data}`);
}
}
// 使用
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notify('Hello World!');
1.2 发布-订阅模式(Pub-Sub Pattern)
发布-订阅模式是观察者模式的变体,通过事件通道解耦发布者和订阅者。
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
unsubscribe(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
}
// 使用
const bus = new EventBus();
const callback1 = data => console.log(`Callback 1: ${data}`);
const callback2 = data => console.log(`Callback 2: ${data}`);
bus.subscribe('message', callback1);
bus.subscribe('message', callback2);
bus.publish('message', 'Hello from EventBus!');
1.3 两种模式对比
实践建议:
- 观察者模式适合组件间紧密耦合的场景
- 发布-订阅模式适合需要解耦的复杂系统
- 现代前端框架(如Vue的EventBus、Redux)大量使用发布-订阅模式
二、工厂模式与单例模式实现
2.1 工厂模式(Factory Pattern)
工厂模式封装了创建对象的逻辑,提供统一的创建接口。
class Car {
constructor(options) {
this.brand = options.brand || 'Unknown';
this.model = options.model || 'Unknown';
}
}
class CarFactory {
create(type, options) {
switch(type) {
case 'sedan':
return new Car({...options, type: 'sedan'});
case 'suv':
return new Car({...options, type: 'suv'});
default:
throw new Error('Unknown car type');
}
}
}
// 使用
const factory = new CarFactory();
const myCar = factory.create('suv', { brand: 'Toyota', model: 'RAV4' });
2.2 单例模式(Singleton Pattern)
确保一个类只有一个实例,并提供全局访问点。
class Database {
constructor(config) {
if (Database.instance) {
return Database.instance;
}
this.connection = this.connect(config);
Database.instance = this;
}
connect(config) {
// 模拟数据库连接
console.log(`Connecting to ${config.url}`);
return { status: 'connected' };
}
static getInstance(config) {
if (!Database.instance) {
Database.instance = new Database(config);
}
return Database.instance;
}
}
// 使用
const db1 = Database.getInstance({ url: 'mongodb://localhost:27017' });
const db2 = Database.getInstance({ url: 'another-connection' });
console.log(db1 === db2); // true
实践建议:
- 工厂模式适合创建复杂对象或需要统一创建逻辑的场景
- 单例模式适合全局状态管理、数据库连接池等场景
- 在JavaScript中,模块系统天然支持单例模式(通过ES Module)
三、装饰器模式(Decorator)与应用
装饰器模式动态地给对象添加额外职责,相比继承更加灵活。
3.1 基本实现
class Coffee {
cost() {
return 5;
}
}
// 装饰器基类
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
}
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 2;
}
}
class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}
}
// 使用
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 8
3.2 ES7装饰器语法
// 类装饰器
function logClass(target) {
console.log(`Class ${target.name} is defined`);
return target;
}
// 方法装饰器
function logMethod(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name} with args: ${args}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
@logClass
class Calculator {
@logMethod
add(a, b) {
return a + b;
}
}
// 使用
const calc = new Calculator();
calc.add(2, 3); // 输出: Calling add with args: 2,3
3.3 实际应用场景
React高阶组件(HOC):
function withLogger(WrappedComponent) { return class extends React.Component { componentDidMount() { console.log(`Component ${WrappedComponent.name} mounted`); } render() { return <WrappedComponent {...this.props} />; } }; }
Redux中间件:
const loggerMiddleware = store => next => action => { console.log('Dispatching:', action); const result = next(action); console.log('Next state:', store.getState()); return result; };
实践建议:
- 装饰器适合横切关注点(日志、权限、缓存等)
- TypeScript已支持装饰器语法(需开启experimentalDecorators)
- 避免过度使用装饰器,以免降低代码可读性
四、代理模式(Proxy高级用法)
Proxy对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举等)。
4.1 基本用法
const target = {
name: 'John',
age: 30
};
const handler = {
get(target, prop) {
if (prop === 'age') {
return `${target[prop]} years old`;
}
return target[prop];
},
set(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new Error('Age must be a number');
}
target[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.age); // "30 years old"
proxy.age = 31; // OK
proxy.age = 'thirty'; // Error: Age must be a number
4.2 高级应用场景
数据验证:
const validator = { set(obj, prop, value) { if (prop === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new Error('Invalid email format'); } obj[prop] = value; return true; } }; const user = new Proxy({}, validator); user.email = 'test@example.com'; // OK user.email = 'invalid-email'; // Error
API缓存:
function createApiCache(api) { const cache = new Map(); return new Proxy(api, { async apply(target, thisArg, args) { const key = JSON.stringify(args); if (cache.has(key)) { console.log('Returning cached result'); return cache.get(key); } const result = await target.apply(thisArg, args); cache.set(key, result); return result; } }); } const cachedFetch = createApiCache(fetch);
负索引数组:
function createNegativeArray(array) { return new Proxy(array, { get(target, prop) { if (typeof prop === 'string' && /^-?\d+$/.test(prop)) { const index = parseInt(prop, 10); return target[index < 0 ? target.length + index : index]; } return Reflect.get(...arguments); } }); } const arr = createNegativeArray(['a', 'b', 'c']); console.log(arr[-1]); // 'c'
4.3 Reflect API配合使用
Reflect提供拦截JavaScript操作的方法,与Proxy完美配合。
const loggedHandler = {
get(target, prop) {
console.log(`Getting property "${prop}"`);
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`Setting property "${prop}" to ${value}`);
return Reflect.set(target, prop, value);
}
};
实践建议:
- Proxy适合实现元编程、数据绑定、验证等高级功能
- 注意性能开销,在性能关键路径慎用
- Vue 3的响应式系统基于Proxy实现
五、设计模式选择指南
模式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
观察者 | 对象状态变化需要通知多个对象 | 松耦合 | 可能引起意外更新 |
发布订阅 | 需要更松耦合的事件系统 | 高度解耦 | 调试困难 |
工厂 | 创建复杂对象 | 封装创建逻辑 | 增加代码复杂度 |
单例 | 全局唯一实例 | 节省资源 | 测试困难 |
装饰器 | 动态添加功能 | 灵活扩展 | 多层装饰降低可读性 |
代理 | 控制对象访问 | 强大拦截能力 | 性能开销 |
最佳实践:
- 优先使用组合而非继承
- 不要强制使用模式,只在真正需要时引入
- 在框架设计中模式更有价值,业务代码保持简单
- JavaScript的函数式特性可以简化许多模式实现
掌握这些设计模式将帮助你构建更健壮、可维护的JavaScript应用,特别是在大型项目和框架开发中。
评论已关闭