在前两篇文章中,我们系统地了解了设计模式的基本概念及其在面向对象编程中的应用,同时也深入探讨了23种经典设计模式的分类和核心要素。在这篇文章中,我们将把焦点转向前端开发领域,探讨在实际开发中广泛使用的 15 种设计模式。
前端开发与其他软件工程领域不同,其面临的挑战和需求具有独特性。例如,用户界面的动态变化、组件的重用、以及复杂的状态管理等问题,都需要特定的设计模式来优化解决。通过了解和应用这些前端设计模式,您可以更高效地处理常见的开发任务,提升代码的可维护性和扩展性,并应对不断变化的需求。
我们将从实际应用出发,详细介绍这些设计模式的背景、核心概念和实际使用场景,帮助您在前端开发中更加得心应手。无论您是新手还是有经验的前端工程师,这些设计模式都将为您的开发实践提供宝贵的指导和支持。
工厂模式
工厂模式是一种创建对象的设计模式,它通过定义一个接口来创建对象,但允许子类决定实例化哪一个类。这使得对象的创建过程可以被子类灵活调整,而不会改变客户端代码。
工厂模式的好处
解耦对象创建:客户端不需要知道创建对象的具体类,只需要知道一个通用接口或基类,从而降低了系统的耦合性。
提高代码可扩展性:可以在不修改现有代码的情况下,添加新的产品类。
简化代码:将对象的创建逻辑封装在工厂方法中,简化了客户端的代码结构。
符合开闭原则:新增对象时,不需要修改工厂接口,而是通过扩展增加对象类型,增强了代码的稳定性和灵活性。
工厂模式的应用场景
对象的创建复杂且逻辑较多时:当对象的创建过程较复杂,且可能涉及到不同的配置或初始化时,可以使用工厂模式简化客户端代码。
处理多种对象创建:当系统需要处理多种不同类型的对象时,工厂模式能够根据条件创建不同的对象。
动态生成类实例:在前端开发中,如根据用户输入或其他动态条件生成特定类型的组件,工厂模式非常适合。
// 动物工厂,根据不同类型生成不同的实例
function AnimalFactory(type, name) {
switch (type) {
case 'cat':
return new Cat(name);
case 'dog':
return new Dog(name);
case 'human':
return new Human(name);
default:
throw new Error(`Type ${type} is not recognized`);
}
}
// Cat 类
function Cat(name) {
this.type = 'cat';
this.name = name;
this.speak = function() {
console.log(`${this.name} says: Meow!`);
}
}
// Dog 类
function Dog(name) {
this.type = 'dog';
this.name = name;
this.speak = function() {
console.log(`${this.name} says: Woof!`);
}
}
// Human 类
function Human(name) {
this.type = 'human';
this.name = name;
this.speak = function() {
console.log(`${this.name} says: Hello!`);
}
}
// 使用工厂创建不同的对象
const myCat = AnimalFactory('cat', 'Kitty');
myCat.speak(); // Kitty says: Meow!
const myDog = AnimalFactory('dog', 'Rex');
myDog.speak(); // Rex says: Woof!
const myHuman = AnimalFactory('human', 'John');
myHuman.speak(); // John says: Hello!
单例模式
确保一个类只有一个实例,并提供全局访问点。在前端开发中, 可以使用单例模式来管理全局状态和资源。 例如在Vue.js应用中,Vuex用于管理应用的全局状态,只存在一个实例。
前端使用单例模式实现的比较出名的库: Vuex、Redux 等全局状态管理库。
实现方式
- 使用字面量可以轻松地创建单例对象。(区别于 Java 因为 Java 需要先定义 class)
const singleton = {
property: "value1",
property2: "value2",
methods1: function(){}
methods2: function(){}
}
- 在 Javascript 中, 每个构造函数都可以用于创建创建单例对象
class Singleton {
static instance: Singleton;
prop1 = "111"
prop2 = "222"
constructor() {
if (Singleton.instance != null) {
return Singleton.instance
}
Singleton.instance = this
}
}
const instance1 = new Singleton()
instance1.prop1 = "000"
const instance2 = new Singleton()
const instance3 = new Singleton()
console.log(instance1, instance2, instance3)
// Singleton { prop1: '000', prop2: '222' }
// Singleton { prop1: '000', prop2: '222' }
// Singleton { prop1: '000', prop2: '222' }
- 通过私有构造函数和静态方法 getInstance() 确保类只能有一个实例,并在首次调用时延迟实例化。
// 不能够使用 new 实例化
// 直接使用 Singleton2.getInstance() 来调用
class Singleton2 {
static instance: Singleton2;
prop1 = "111"
prop2 = "222"
static getInstance() {
if (Singleton2.instance != null) {
return Singleton2.instance
}
Singleton2.instance = new Singleton2()
return Singleton2.instance
}
private constructor() {
}
}
const obj = Singleton2.getInstance()
const obj2 = Singleton2.getInstance()
obj === obj2 // true
// 不允许
const obj3 = new Singleton2() // 类“Singleton2”的构造函数是私有的,仅可在类声明中访问。ts(2673)
发布-订阅模式
前端中用的比较多 (event、 dom event)
发布订阅模式 (Publish-Subscribe Pattern) 也叫消息队列模式,它是一种将发布者和订阅者解耦的设计模式。在前端开发中, 可以使用发布订阅模式来实现组件之间的通信。
经典案例: vue 的事件总线, Redux 中的 store, Publish.js、Eventbus
应用场景: 大文件上传、总线通信等。
document.addEventListener('click',()=>{}) // 发布订阅模式, 订阅的 API
// 创建一个 EventListener
class MyEvent {
private eventMap: Record<string, Array<Function>> = {}
// 仿 addEventListener
addEvent(eventName: string, callback: Function) {
if (!this.eventMap[eventName]) {
this.eventMap[eventName] = []
}
this.eventMap[eventName].push(callback)
}
removeEvent(eventName: string, callback: Function) {
if (!this.eventMap[eventName]) {
return
}
this.eventMap[eventName] = this.eventMap[eventName].filter(fn => fn !== callback)
}
triggerEvent(eventName: string, ...data: any) {
if (!this.eventMap[eventName]) {
return
}
this.eventMap[eventName].forEach(fn => fn(...data)) // 约定展开参数
}
}
const myEvent = new MyEvent()
myEvent.addEvent("click", (...data: any) => console.log("click1", data))
myEvent.addEvent("click", (...data: any) => console.log("click2"))
const click3Cb = () => console.log("click3")
myEvent.addEvent("click", click3Cb) // 注意 两个相同的箭头函数是不相等的, 要用变量来保存, 内存地址比较
myEvent.removeEvent("click", click3Cb)
myEvent.addEvent("move", (...data: any) => console.log("move1"))
myEvent.addEvent("move", (...data: any) => console.log("move2"))
myEvent.addEvent("move", (...data: any) => console.log("move3"))
myEvent.triggerEvent("click", 1, 2, 3) // click1 [ 1, 2, 3 ] click2
实现了一个简单的自定义事件监听器 MyEvent,类似于浏览器的 addEventListener 和 removeEventListener 功能。MyEvent 使用 eventMap 存储事件名称与回调函数的映射。你可以通过 addEvent 方法为某个事件添加多个回调,通过 removeEvent 方法移除指定的回调,并通过 triggerEvent 触发对应的事件,传递参数给所有注册的回调函数。
观察者模式
观察者模式和发布订阅模式在前端开发中都非常常见,但两者有细微区别。观察者模式不需要 eventName,可以理解为一种简化版的发布订阅模式。观察者模式中的 subject 只关注一个单一的 channel,即所有事件类型只有一种,没有复杂的事件分类。而发布订阅模式则支持多个 eventName,允许更复杂的事件管理。在一些数据流管理场景下,Rx.js 等工具对业务逻辑的处理显得尤为重要,特别是在需要处理异步流的复杂应用中。
观察者模式 Observer Pattern: 当对象间存在一对多的关系时, 使用观察者模式。当被观察的对象发生变化时, 其所有的观察者都会收到通知并进行相应的操作。在 JavaScript 中, 可以使用回调函数或事件来监听实现观察者模式。
在前端开发中, 观察者模式常备用来实现组件间的数据传递和事件处理。比如, 当一个组件的状态发生改变时, 可以通过观察者模式来通知其他组件更新自身的状态或视图。
在观察者模式中,通常会定义两种角色: 观察者(Observer)和 被观察者/主题(Subject)
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((obs) => obs.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, observers!');
subject.removeObserver(observer1);
subject.notify('Goodbye, observers!');
装饰者模式
注解(Annotation)是一种用于为代码提供元数据的特殊语法标记,通常不影响程序的运行,但可用于编译时、运行时进行额外处理或行为控制。
前端常用较多: Nest.js、Angular
装饰者模式 (Decorator Pattern): 动态地给一个对象添加额外的职责, 在前端开发中, 可以使用装饰者模式来动态修改组件的行为和样式。
JavaScript 中的装饰者模式可以通过以下几种方式实现:
对象的装饰器的典型实现
const obj = {
foo(){
console.log("foo")
}
}
function barDecorator(obj){
obj.bar = function(){
console.log("bar")
}
return obj
}
const decorateObj = barDecorator(obj) // @barDecorator
decorateObj.foo()
decorateObj.bar()
nest.js 代码节选
import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';
// 控制器的基本定义,`@Controller()` 装饰器定义了路由的前缀,这里是 "users"
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
// 处理 HTTP GET 请求的路由,路径为 `/users`
@Get()
findAll() {
// 调用服务中的 `findAll` 方法,返回用户列表
return this.userService.findAll();
}
// 处理动态路由,路径为 `/users/:id`
@Get(':id')
findOne(@Param('id') id: string) {
// 获取请求中的 `id` 参数,并调用服务中的 `findOne` 方法,查找特定用户
return this.userService.findOne(id);
}
}
命令模式
命令模式 (Command Pattern) 是一种将操作封装成对象的设计模式,这样你可以把方法调用、参数都打包在一起。简单来说,就是把操作变成一个可以传来传去的小对象,方便存储、执行、甚至撤销和重做。
在前端开发中,命令模式特别适合那些需要 撤销 和 重做 的场景。比如在文本编辑器里,每次用户修改文本,你可以创建一个命令对象,把这个操作记录下来。需要撤销时,从历史记录里找到最新的命令,执行它的“反向操作”就行了。这样,不管用户做了多少次修改,你都能轻松处理撤销和重做。
这背后还可以有个 命令处理器,专门用来管理这些命令,像一个聪明的小管家,帮你处理操作记录,让系统更灵活更强大。
class Command {
executed = false
constructor(private receiver: Receiver, private args: any) {
}
execute() {
if (this.executed) return
this.receiver.execute(this.args)
this.executed = true
}
undo() {
if (!this.executed) return
this.receiver.undo(this.args)
this.executed = false
}
}
class Receiver {
private value = 0
execute(args?: any) {
this.value += args
}
undo(args?: any) {
this.value -= args
}
}
const receiver = new Receiver()
const command1 = new Command(receiver, 1)
const command2 = new Command(receiver, -1)
command1.execute()
console.log(receiver)
command2.execute()
console.log(receiver)
结语
通过本篇文章的探索,我们已经初步掌握了前端开发中常用的几种设计模式。在接下来的学习中,我们会继续深入,解锁更多实用的设计模式,如迭代器模式、原型模式、职责链模式等,看看它们是如何帮助我们优化复杂的业务逻辑和项目架构。前端开发是个不断积累的过程,设计模式也像是开发中的一把把利器,学会了,你就能更从容地应对各种挑战。准备好,下一篇我们继续“开箱”新工具吧!