JS设计模式之装饰者模式:优雅的给对象增添“魔法”

引言
在前端开发中,我们经常会遇到需要在不修改已有代码的基础上给对象添加新的行为或功能的情况。而传统的继承方式并不适合这种需求,因为继承会导致类的数量急剧增加,且每一个子类都会固定地实现一种特定的功能扩展。
装饰者模式则提供了一种更加灵活的解决方案。它可以在运行时动态地给对象新的功能,同时避免了继承带来的类爆炸问题。
本篇文章将会详细介绍 JavaScript 设计模式装饰者模式。通过阅读本文,你将了解到如使用装饰者模式来动态地扩展对象的功能,同时保持代码的灵活性和可维护性。
一. 什么是装饰者模式
定义
装饰者模式(Decorator Pattern)是一种结构型设计模式,它允许将对象包在其他对象中,而无需改变对象的原始结构,从而动态地为其添加新的行为和功能,并且不会改变原有对象的结构。
装饰者模式通过将对象包装到一个装饰器中,将新的行为包裹在原始对象周围,以增强其功能。这种模式通过使用组合而不是继承的方式,可以在运行时动态地添加、删除或修改对象的功能。
核心思想
装饰者模式的核心思想是通过组合来实现功能的扩展,而不是通过继承。通过将对象包装进装饰器对象中,可以在需要的时候像堆叠木块一样一层层地添加功能,也可以在不需要某个功能时轻松地移除它。
使用装饰者模式可以提供灵活性和可扩展性,同时也遵循开闭原则,即对修改关闭,对扩展开放。它可以帮助我们避免继承链的臃肿和复杂化,将对象的功能拆分为不同的装饰器,使得代码更加可维护和可复用。
主要特点
装饰者模式具有以下主要特点:
-
动态添加行为:装饰者模式允许在运行时动态地向对象添加新的行为,而不需要修改已有的代码或对象的结构。通过将对象包装在装饰器中,可以在不改变原始对象的情况下,增加、修改或删除对象的功能。
-
组合而非继承:装饰者模式通过组合而不是继承的方式,实现了对对象的功能扩展。不同于继承链的方式,装饰者模式允许你根据需要灵活地组合多个装饰器,以实现不同的功能组合。
-
透明性:装饰者模式使得装饰器和原始对象具有相同的接口,这意味着对于使用对象的客户端来说,无论是使用原始对象还是装饰器对象,都可以一致地进行操作,而不会产生任何混淆。
-
可逆性:装饰者模式允许随时添加、删除或修改对象的行为,因此具有可逆性。即如果你不再需要某个装饰器的功能,可以很容易地将其从装饰器堆栈中移除,恢复到原始对象的状态。
-
灵活性和可扩展性:通过装饰者模式,可以灵活地扩展对象的功能,而无需修改原始对象或其他装饰器。你可以根据需要组合不同的装饰器,构建出复杂的功能组合,同时保持代码的可维护性和可复用性。
总的来说,装饰者模式提供了一种灵活、可扩展和可逆的方式来动态地添加对象的行为。它使得功能扩展变得简单,同时通过组合而非继承的方式,避免了继承链的复杂性和僵化性。这使得代码更加灵活、可维护和可复用。
二. 装饰者模式的结构
装饰者模式的结构主要包含以下几个角色:
-
组件(Component):定义一个接口或抽象类,作为装饰器和具体组件对象的公共接口。它可以是一个类或者接口,它声明了具体组件和装饰器需要实现的方法。
-
具体组件(ConcreteComponent):实现了组件接口,也就是被装饰的对象。它是原始对象,具有基本的功能。
-
装饰器(Decorator):实现了组件接口,并持有一个被装饰的组件对象的引用。装饰器通过对被装饰对象的包装,可以在不修改原始对象的基础上,动态地添加额外的行为。它具有与组件相同的接口,可以递归地包装其他装饰器或具体组件。
-
具体装饰器(ConcreteDecorator):扩展了装饰器类,实现了具体的装饰逻辑。具体装饰器可以在调用被装饰对象的方法之前或之后,添加额外的行为。
下面是 JavaScript 装饰者模式的结构示意图:

在这个结构中,具体组件(ConcreteComponent)是被装饰的原始对象,它实现了组件接口,并具有基本的功能。装饰器(Decorator)也实现了组件接口,并持有一个被装饰的组件对象的引用。具体装饰器(ConcreteDecorator)扩展了装饰器类,可以在调用被装饰对象的方法之前或之后,添加额外的行为。
通过组件接口的统一,装饰器和具体组件对象可以互相替换,使得客户端可以透明地使用装饰后的对象。可以根据需求灵活地组合装饰器,实现不同的功能组合。
注意:装饰者模式中的装饰器和具体组件对象之间是松耦合的关系,它们之间通过共同的接口进行交互,不依赖具体的实现,从而实现了动态扩展和变更功能的目的。
三. 如何实现装饰者模式
装饰者模式的实现步骤可以分为以下几个步骤:
-
定义组件接口或抽象类:
定义一个基础的组件接口或抽象类,它是被装饰者和装饰器共同实现的接口。
// 组件接口或抽象类
class Component {operation() {}
}
-
实现具体组件类:
创建一个实现了组件接口或抽象类的具体组件类,也就是被装饰者。
// 具体组件类
class ConcreteComponent extends Component {operation() {console.log("执行具体组件的操作");}
}
-
定义装饰器抽象类:
创建一个装饰器抽象类,继承自组件接口或抽象类,它将持有一个被装饰的组件对象。
// 装饰器抽象类
class Decorator extends Component {constructor(component) {super();this.component = component;}operation() {this.component.operation();}
}
-
实现具体装饰器类:
创建具体的装饰器类,继承自装饰器抽象类,可以在不修改原有对象的情况下,为它添加额外的行为。
// 具体装饰器类A
class ConcreteDecoratorA extends Decorator {operation() {super.operation();this.addBehaviorA();}addBehaviorA() {console.log("添加额外的行为A");}
}// 具体装饰器类B
class ConcreteDecoratorB extends Decorator {operation() {super.operation();this.addBehaviorB();}addBehaviorB() {console.log("添加额外的行为B");}
}
-
创建装饰链:
可以按需创建装饰链,将具体的装饰器对象以特定的顺序组合在一起。
// 创建被装饰的具体组件对象
const component = new ConcreteComponent();// 创建具体装饰器对象A,并传入component
const decoratorA = new ConcreteDecoratorA(component);// 创建具体装饰器对象B,并传入decoratorA
const decoratorB = new ConcreteDecoratorB(decoratorA);
-
调用装饰后的对象方法:
通过装饰后的对象来调用方法,观察装饰器是否成功地添加了额外的行为。
// 调用装饰后的对象的方法
decoratorB.operation();
在上述步骤中,我们首先定义了一个抽象类 Component 作为组件的基本接口。然后创建了具体组件类 ConcreteComponent,实现了组件接口的 operation 方法。
接下来,我们定义了一个装饰器抽象类 Decorator,其构造函数接收一个组件对象,通过调用组件对象的 operation 方法来实现组件的操作。
然后,我们创建了两个具体装饰器类 ConcreteDecoratorA 和 ConcreteDecoratorB,它们继承自装饰器抽象类,并实现了自己的增加行为的方法。
最后,我们创建了被装饰的具体组件对象 component,然后按照一定顺序创建了具体装饰器对象 decoratorA 和 decoratorB,并将它们串联起来形成装饰链。最后,调用装饰后的对象 decoratorB 的 operation 方法,观察它的输出。
这样,通过装饰器模式,我们可以在不改变原有对象的情况下,动态地扩展对象的功能。
四. 装饰者模式的应用场景
在 JavaScript 中,装饰者模式可以应用于各种场景,用于动态地给对象添加额外的功能或行为。以下是一个详细的代码分析,展示了 JavaScript 装饰者模式是如何应用的。
假设我们有一个简单的组件,用于展示用户的个人信息,包括姓名、年龄和职业。我们希望能够根据用户的权限动态地添加一些额外功能,比如显示用户的手机号码或地址,同时保持代码的灵活性。
-
首先,我们创建一个基础的用户信息组件
UserInfo:
class UserInfo {constructor(name, age, occupation) {this.name = name;this.age = age;this.occupation = occupation;}render() {console.log(`Name: ${this.name}`);console.log(`Age: ${this.age}`);console.log(`Occupation: ${this.occupation}`);}
}
-
然后,我们创建一个装饰者类
PhoneDecorator,用于在用户信息中添加显示手机号码的功能:
class PhoneDecorator {constructor(userInfo, phoneNumber) {this.userInfo = userInfo;this.phoneNumber = phoneNumber;}render() {this.userInfo.render();console.log(`Phone: ${this.phoneNumber}`);}
}
-
接下来,我们创建另一个装饰者类
AddressDecorator,用于在用户信息中添加显示地址的功能:
class AddressDecorator {constructor(userInfo, address) {this.userInfo = userInfo;this.address = address;}render() {this.userInfo.render();console.log(`Address: ${this.address}`);}
}
-
现在,我们可以使用这些装饰者来动态地添加功能。下面是应用装饰者模式的示例代码:
// 创建基础的用户信息对象
const userInfo = new UserInfo("John Doe", 30, "Engineer");// 创建装饰者对象并应用装饰器
const phoneDecorator = new PhoneDecorator(userInfo, "1234567890");
const addressDecorator = new AddressDecorator(phoneDecorator, "123 Main St");// 渲染用户信息
addressDecorator.render();
输出结果将会是:
Name: John Doe
Age: 30
Occupation: Engineer
Phone: 1234567890
Address: 123 Main St
通过使用装饰者模式,我们可以动态地为用户信息对象添加不同的功能组合,而不需要修改原始对象,从而实现了代码的灵活性和扩展性。
注意:以上代码只是一个简化的示例,实际应用中可能会更加复杂。装饰者模式的实现也可以有很多变体,具体的实现方式可以根据需求和设计的复杂度来决定。
五. 装饰者模式的优缺点
装饰者模式的优点
-
动态扩展:装饰者模式允许在运行时动态为对象添加新的功能或行为,而无需修改原始对象的结构。这使得代码更加灵活,能够根据需求动态地组合和应用装饰器。
-
开闭原则:装饰者模式符合开闭原则,可以在不修改已有代码的情况下扩展新的功能。通过添加新的装饰器,可以在不改变原始对象的代码的前提下,扩展和修改对象的行为。
-
单一职责原则:装饰者模式将具体功能的实现分散到不同的装饰器类中,每个装饰器只关注自己的功能实现,使得代码结构清晰,符合单一职责原则。
-
组合灵活:通过灵活组合不同的装饰器,可以实现多种功能组合,满足不同的需求。装饰器模式提供了一种仅通过不同的组合方式就可以实现复杂功能的便捷方法。
装饰者模式的缺点
-
复杂性增加:装饰者模式引入了大量的类和对象,使得代码结构变得复杂。在设计和理解装饰器链的同时,需要考虑装饰器之间的关系和顺序。
-
运行时性能开销:由于装饰者模式是通过多层嵌套的方式来添加功能的,每个装饰器都会增加一次方法调用的开销。这可能在性能敏感的场景中产生一定的性能损失。
-
额外对象的创建:每个装饰器需要持有一个被装饰的对象,这样会增加多个对象的创建和维护成本。在需要大量对象时,可能会占用较多的内存空间。
综上所述,装饰者模式提供了动态扩展和灵活组合功能的方式,符合开闭原则和单一职责原则,但也可能导致代码复杂性增加和运行时性能开销。在使用时需要根据实际情况进行权衡和选择。
总结
在本篇文章中,我们详细解析了 JavaScript 中的装饰者模式及其应用。装饰者模式是一种结构型设计模式,通过动态地给对象添加新的行为,实现了功能的扩展和组合,同时遵循开闭原则和单一职责原则。
在使用装饰者模式时,我们需要注意合理地设计装饰者类的层级结构,避免过多的装饰者嵌套导致代码复杂度的增加。同时,要确保每个装饰者类的职责单一,只关注一个特定的功能扩展。
装饰者模式在实际开发中有着广泛的应用,如日志记录、性能监测、权限验证等。它不仅提供了一种灵活的扩展方式,还能帮助我们解耦和复用代码。
通过学习和理解装饰者模式,我们能够更加灵活地设计和开发 JavaScript 应用,提高代码的可扩展性和维护性。

相关文章:
JS设计模式之装饰者模式:优雅的给对象增添“魔法”
引言 在前端开发中,我们经常会遇到需要在不修改已有代码的基础上给对象添加新的行为或功能的情况。而传统的继承方式并不适合这种需求,因为继承会导致类的数量急剧增加,且每一个子类都会固定地实现一种特定的功能扩展。 装饰者模式则提供了…...
准备好了吗?JAVA从业AI开发的学习路线详解
作为一个拥有扎实 Java 基础的人,想要涉足人工智能(AI)应用开发,你已经在编程能力方面打下了很好的基础。Java 是一种通用的、强类型的语言,非常适合于开发高性能的应用程序,尤其是在后端服务和大规模分布式…...
神经网络通俗理解学习笔记(1)
神经网络通俗理解学习笔记(1) 神经网络原理激活函数前向传播和反向传播多层感知机代码实现加载数据网络结构损失函数优化器训练测试保存 回归问题一元线性回归多元线性回归多项式回归 线性回归代码实现数据生成设置超参数初始化参数可视化Pytorch模型实现…...
有n个人,他们需要分配m元钱(m>n),每个人至少分到1元钱,且每个人分到的钱数必须是整数。请问有多少种分配方案?
分配方案 描述 有n个人,他们需要分配m元钱(m>n),每个人至少分到1元钱,且每个人分到的钱数必须是整数。请问有多少种分配方案? 输入 一行,两个整数,分别是人数n与钱数m,用一个空格隔开。 输出 一行&am…...
光耦——创新引擎 助推中国经济高质量发展
近年来,中国经济正处于转型升级的关键时期,高质量发展成为经济发展的重要目标。在这一伟大征程中,光耦作为一种关键性的电子元器件,正在发挥着重要的作用,助力中国经济迈向更加光明的未来。 光耦概念及工作原理 ▲光耦…...
Go 中 RPC 的使用教程
前言 RPC(Remote Procedure Call)是一种允许程序调用远程服务器上函数的方法,调用过程对于开发者来说像是调用本地函数一样方便。Go 语言自带了强大的 net/rpc 库,能够让开发者轻松实现基于 Go 的 RPC 服务。本文将介绍 Go 中 RP…...
挖耳勺可以伸进耳朵多深?安全可视挖耳勺推荐!
一般来说,挖耳勺不应该伸进耳朵太深,外耳道的长度大约在2.5厘米到3.5厘米之间,但不建议将挖耳勺伸进超过外耳道外1/3的深度,也就是大概1厘米左右较为安全。因为如果伸得太深,很容易损伤外耳道皮肤,引起疼痛…...
SuperMap GIS基础产品FAQ集锦(20240911)
一、SuperMap iObjects Java 问题1:【iObject Python】Objects Python产品有哪些能力特性和优势? 11.2.0 【解决办法】iObjects Python产品包含传统GIS功能(基于iObjects Java扩展的功能接口)和AI GIS功能模块。 其中传统GIS功能…...
从状态管理到性能优化:全面解析 Android Compose
文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compo…...
ChatGPT提示词优化大师使用指南
我希望你成为我的ChatGPT提示词优化大师。 您的目标是帮助我根据自己的需要制定尽可能最好的提示。 你提供的提示应该是站在我向ChatGPT发起请求的角度来写的。我的初始提示词如下:此处填入你的初始提示词 ChatGPT提示词生成器 我希望你充当提示词生成器。 比如&…...
计算机毕业设计 智能推荐旅游平台 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试
🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点…...
【拥抱AI】基于多种数据分段工具的优缺点分析
最近在深入了解RAG方面的知识,其中数据清洗和数据分段是创建知识库的重要步骤。数据清洗目前暂时选用了MinerU,然后就需要针对数据分段进行选型。 以下是我了解到的几种数据分段工具,简单总结了一下它们的优缺点,权当笔记分享&am…...
在 Windows 系统上,文件传输到虚拟机(VM)可以通过 VS Code 的图形界面(GUI)或命令行工具进行操作
在 Windows 系统上,文件传输到虚拟机(VM)可以通过 VS Code 的图形界面(GUI)或命令行工具进行操作。以下是几种方法: ### 方法 1: 使用 VS Code 图形界面 1. **连接到远程 VM**: - 在 VS Cod…...
kafka的主要功能
Apache Kafka 是一个分布式流处理平台,它最初由 LinkedIn 开发,后来捐赠给了 Apache Software Foundation,并成为了 Apache 的顶级项目。Kafka 设计用于处理实时数据流,并且提供了高性能、可扩展性和持久性。下面是 Kafka 的主要功…...
vue3中provide和inject详解
provide和inject是什么 provide 和 inject 是 Vue.js 框架中提供的一种依赖注入机制。这种机制允许一个祖先组件(提供者)向其所有子孙组件(使用者)提供数据或方法,而不需要通过逐层组件传递属性(props&…...
相约华中科技大学,移动云技术论坛来了!NineData创始人CEO叶正盛将分享《数据库全球实时传输技术实践》的主题演讲
2024年9月12日,中国移动云能力中心将在华中科技大学举办“智算浪潮下数据库发展论坛”,共同探讨数据库技术与应用的创新,分享算力网络时代数据库未来发展的洞见。本次论坛,NineData 创始人&CEO 叶正盛受邀参会,并来…...
华为 昇腾 310P 系列 AI 处理器支持 140Tops 的 AI 算力。
1、产品简介 模组是基于昇腾 310P 系列 AI 处理器设计而成,可实现图像、视频等多种数据分析 与推理计算。超强的视频编解码能力以及支持 140Tops 的 AI 算力。在边缘侧及端侧的嵌入式计算 领域,有着极高的性价比,具有超强算力、 超高能效、…...
基于单片机的小型生态鱼缸控制器设计
本设计以STC89C52单片机为核心,利用DS18B20温度传感器和LCD1602液晶显示器实时采集和显示当前环境温度,并根据与预设温度阈值的比较结果控制加热棒或风扇进行加热或制冷操作。此外,该控制器还利用DS1302完成计时功能,在预设时间点…...
git-repo使用
即使用 XML 格式文件(manifest 清单文件)定义一个项目的多仓库关联,然后用 repo 客户端工具操作多仓库 git repo命令行格式: git repo <子命令> <参数>创建一个空目录,作为工作区。 $ mkdir workspace$ …...
如何设计实现完成一个FPGA项目
设计并完成一个FPGA项目是一个复杂但非常有价值的工程任务。以下是一个详细的步骤指南,帮助你从零开始完成一个FPGA项目。 1. 项目定义与需求分析 确定项目目标:明确项目要实现的功能和性能指标。需求分析:列出所有功能需求、性能需求、接口需求等。可行性分析:评估技术可…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...
Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
【Elasticsearch】Elasticsearch 在大数据生态圈的地位 实践经验
Elasticsearch 在大数据生态圈的地位 & 实践经验 1.Elasticsearch 的优势1.1 Elasticsearch 解决的核心问题1.1.1 传统方案的短板1.1.2 Elasticsearch 的解决方案 1.2 与大数据组件的对比优势1.3 关键优势技术支撑1.4 Elasticsearch 的竞品1.4.1 全文搜索领域1.4.2 日志分析…...
Appium下载安装配置保姆教程(图文详解)
目录 一、Appium软件介绍 1.特点 2.工作原理 3.应用场景 二、环境准备 安装 Node.js 安装 Appium 安装 JDK 安装 Android SDK 安装Python及依赖包 三、安装教程 1.Node.js安装 1.1.下载Node 1.2.安装程序 1.3.配置npm仓储和缓存 1.4. 配置环境 1.5.测试Node.j…...
