JavaScript 中的设计模式是用来解决常见问题的最佳实践方案。这些模式有助于创建可重用、易于理解和维护的代码。下面列出了一些常见的 JavaScript 设计模式及其代码示例。
1. 单例模式(Singleton)
单例模式确保一个类仅有一个实例,并提供一个全局访问点。
class Singleton {
static instance = null;
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
// 初始化代码
this.data = {};
}
getData() {
return this.data;
}
setData(data) {
this.data = data;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
2. 工厂模式(Factory)
工厂模式用于创建对象,而无需指定具体类。
function createProduct(type) {
switch(type) {
case 'car':
return new Car();
case 'bike':
return new Bike();
default:
return null;
}
}
class Car {
constructor() {
this.type = 'Car';
}
}
class Bike {
constructor() {
this.type = 'Bike';
}
}
const car = createProduct('car');
console.log(car.type); // Car
5. 代理模式(Proxy)
JavaScript ES6 引入了 Proxy 对象来定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
let target = {};
let handler = {
get: function(target, prop, receiver) {
if (prop in target) {
return target[prop];
}
return `Unknown property '${prop}'`;
}
};
let proxy = new Proxy(target, handler);
console.log(proxy.foo); // "Unknown property 'foo'"
4.发布订阅模式与观察者模式
在 JavaScript 中,发布/订阅模式(Pub/Sub)和观察者模式(Observer Pattern)经常被视为相似或相关的设计模式,但它们之间确实存在一些细微的差别和不同的使用场景。尽管它们在很多方面都有重叠,但理解它们之间的差异对于正确选择和应用这些模式至关重要。
观察者模式(Observer Pattern)
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
- 主题(Subject):管理所有依赖于它的观察者对象,并在其内部状态发生改变时主动通知观察者。
- 观察者(Observer):为那些在主题对象发生改变时需要获得通知的对象提供一个统一的接口。
发布/订阅模式(Pub/Sub)
发布/订阅模式是一种消息传递模式,消息发送者(发布者)不会将消息直接发送给特定的接收者(订阅者)。相反,发布者将发布的消息分为不同的类别,并且无需了解哪些订阅者(如果有的话)会收到这个消息。同样,订阅者可以表达对一个或多个类别的兴趣,并且只接收感兴趣的消息,无需了解哪些发布者(如果有的话)会发布这个消息。
- 发布者(Publisher):不直接将消息发送给特定的订阅者,而是发布的消息到“频道”或“主题”。
- 消息代理(Message Broker):负责接收来自发布者的消息,并将这些消息分发给所有订阅了相应主题的订阅者。
- 订阅者(Subscriber):表达对一个或多个特定主题的兴趣,并接收来自这些主题的消息。
差异
- 解耦程度:虽然两者都提供了一定程度的解耦,但发布/订阅模式通常提供更高级别的解耦。在观察者模式中,观察者和主题之间通常存在直接的依赖关系,而在发布/订阅模式中,发布者和订阅者之间不需要知道对方的存在。
- 中介角色:在发布/订阅模式中,通常需要一个消息代理来作为中介,负责接收发布者的消息并将其分发给订阅者。而在观察者模式中,主题直接通知观察者,没有中介。
- 消息类型:在订阅发布/模式中,消息通常通过主题或频道进行分类,订阅者可以订阅一个或多个主题。在观察者模式中,观察者通常直接监听主题对象的特定状态变化。
示例代码(简化)
这里是一个简化的 JavaScript 示例,用于展示两种模式的实现:
观察者模式
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => {
observer.update(data);
});
}
}
class Observer {
update(data) {
console.log(`Observer received: ${data}`);
}
}
// 使用...
发布/订阅模式(简化,实际中可能需要更复杂的实现)
// 创建一个简单的发布订阅者管理类
class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件的字典,键为事件名,值为订阅者数组
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = []; // 如果该事件尚未被订阅,则初始化一个空数组
}
this.events[eventName].push(callback); // 将回调函数添加到订阅者数组中
return this; // 支持链式调用
}
// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
// 移除指定的回调函数
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
// 如果订阅者数组为空,则删除该事件
if (this.events[eventName].length === 0) {
delete this.events[eventName];
}
}
return this; // 支持链式调用
}
// 发布事件
emit(eventName, ...args) {
if (this.events[eventName]) {
// 遍历订阅者数组,并执行每个回调函数
this.events[eventName].forEach(callback => {
callback.apply(this, args);
});
}
return this; // 支持链式调用
}
// 可选:监听一次后自动取消订阅
once(eventName, callback) {
const onceCallback = (...args) => {
callback.apply(this, args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
return this; // 支持链式调用
}
}
// 使用示例
const eventBus = new EventEmitter();
// 订阅者1
function subscriber1(price) {
console.log('订阅者1收到消息:当前价格已降至' + price + '元');
}
// 订阅者2
function subscriber2(price) {
console.log('订阅者2也收到消息:价格更新为' + price + '元');
}
// 订阅事件
eventBus.on('priceUpdate', subscriber1);
eventBus.on('priceUpdate', subscriber2);
// 使用once方法监听一次
eventBus.once('specialOffer', (offer) => {
console.log('只接收一次的特别优惠:' + offer);
});
// 发布事件
eventBus.emit('priceUpdate', 99); // 订阅者1和订阅者2都会收到消息
eventBus.emit('specialOffer', '买一赠一'); // 只有一个订阅者会收到这个特别优惠的消息
// 取消订阅
eventBus.off('priceUpdate', subscriber1);
eventBus.emit('priceUpdate', 88); // 只有订阅者2会收到消息
应用场景
- 观察者模式:
- 多用于单个应用内部,特别是在需要实现对象间的一对多依赖关系时。
- 例如,在图形用户界面(GUI)框架中,按钮的点击事件、窗口的打开和关闭事件等都可以使用观察者模式进行处理。
- 另一个例子是股票市场,股票交易所可以充当被观察者,而股票交易员可以充当观察者,当股票价格、交易量等发生变化时,交易员将接收到通知。
- 发布/订阅模式:
- 更多的是一种跨应用的模式(cross-application pattern),适用于需要在不同系统或组件之间进行通信的场景。
- 例如,在微服务架构中,服务之间的通信经常采用发布/订阅模式,通过消息队列或事件总线进行异步通信。
- 在前端开发中,发布/订阅模式也常用于实现组件间的通信,特别是在Vue、React等现代前端框架中,通过事件总线或全局状态管理库(如Redux、Vuex)来实现跨组件的通信。
- 自定义事件:在前端开发中,可以创建自定义事件,让组件或模块之间通过事件进行通信。
- 状态管理:在前端状态管理库(如Redux、Vuex)中,发布订阅模式是实现状态更新的核心机制。
- 异步编程:在异步编程中,发布订阅模式可以帮助我们解耦异步操作和它们的回调函数。
- 插件系统:在插件系统中,发布订阅模式可以让插件之间以解耦的方式进行通信。
3. 策略模式(Strategy)
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
一、定义与原理
策略模式定义了一系列的算法,并将每个算法封装起来,使它们可以相互替换。在JavaScript中,策略模式通常通过函数或对象来实现,每个策略都是一个独立的函数或对象,封装了具体的算法逻辑。客户端可以根据需要选择不同的策略来执行相应的算法。
二、结构组成
一个基于策略模式的程序通常包含以下两部分:
- 策略类(Strategy):封装了具体的算法或行为,每个策略类都实现了相同的接口(在JavaScript中通常是通过函数参数或方法签名来保证的)。
- 环境类(Context):也被称为上下文类,它接受客户的请求,并根据需要选择合适的策略类来执行算法。环境类维护了一个对策略对象的引用,并在需要时调用其算法。
三、应用场景
策略模式在JavaScript中有着广泛的应用场景,包括但不限于:
- 表单验证:根据不同的验证规则创建不同的策略对象,并将其注入到表单验证器中,实现灵活的表单验证。
- 排序算法:将排序算法封装在不同的策略对象中,根据需要选择不同的排序算法。
- 动画效果:将不同的动画算法封装在独立的策略对象中,根据用户的操作来动态地选择不同的动画效果。
- 缓动函数:封装不同的缓动算法,使缓动函数可以根据不同的缓动算法来执行不同的效果。
class Strategy {
doOperation(num1, num2) {
throw new Error('This method must be overridden!');
}
}
class AddStrategy extends Strategy {
doOperation(num1, num2) {
return num1 + num2;
}
}
class SubtractStrategy extends Strategy {
doOperation(num1, num2) {
return num1 - num2;
}
}
class Context {
constructor(strategy) {
this.strategy = strategy;
}
executeStrategy(num1, num2) {
return this.strategy.doOperation(num1, num2);
}
}
const context = new Context(new AddStrategy());
console.log(context.executeStrategy(5, 3)); // 8
context.strategy = new SubtractStrategy();
console.log(context.executeStrategy(5, 3)); // 2