基础知识3.0
异步加载和延迟加载
在前端开发中,异步加载(Asynchronous Loading) 和 延迟加载(Deferred Loading/Lazy Loading) 是两种提升页面性能和用户体验的关键技术。它们都旨在优化资源加载策略,但实现方式和应用场景有所不同。以下是详细解析:
一、异步加载(Asynchronous Loading)
1. 定义
异步加载 是指资源(如 JavaScript 文件)的加载过程不会阻塞页面的解析和渲染。
资源加载与页面解析并行进行,加载完成后立即执行。
2. 实现方式
- JavaScript 的
async
属性:
<script src="script.js" async></script>
行为:脚本异步加载,下载完成后立即执行(可能中断 HTML 解析)。
- 适用场景:独立脚本(如统计代码、广告脚本),不依赖其他脚本或 DOM。
3. 特点
不阻塞渲染:HTML 解析和脚本下载并行。
无序执行:多个
async
脚本的执行顺序无法保证。适用性:适合无需等待 DOM 或其他脚本的代码。
二、延迟加载(Deferred Loading)
1. 定义
延迟加载 是指将资源的加载和执行推迟到合适的时机(如页面解析完成或用户触发某些操作)。
分为两种类型:
JavaScript 的
defer
属性:延迟脚本执行。资源懒加载(Lazy Loading):延迟加载非关键资源(如图片、视频)。
2. JavaScript 的 defer
属性
<script src="script.js" defer></script>
行为:脚本异步下载,但执行会延迟到 HTML 解析完成后(
DOMContentLoaded
事件前)。特点:
保持脚本执行顺序(按文档中的顺序)。
适用于依赖 DOM 或其他脚本的代码(如页面初始化逻辑)。
3. 资源懒加载(Lazy Loading)
定义:延迟加载非关键资源(如图片、视频),直到它们进入视口(Viewport)或即将被用户看到时再加载。
实现方式:
- HTML
loading="lazy"
属性(现代浏览器支持):
- HTML
<img src="placeholder.jpg" data-src="image.jpg" loading="lazy" alt="Lazy Image">
- Intersection Observer API(编程式控制):
const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
images.forEach(img => observer.observe(img));
第三方库:如
lozad.js
。适用场景:
长页面中的图片或视频。
首屏外的内容(如用户滚动后加载)。
三、对比:异步加载 vs 延迟加载
特性 | 异步加载(async) | 延迟加载(defer/Lazy Loading) |
---|---|---|
资源类型 | 主要用于 JavaScript | JavaScript、图片、视频等资源 |
执行时机 | 下载完成后立即执行 | defer:HTML 解析后执行;懒加载:资源进入视口后加载 |
阻塞渲染 | 不阻塞 HTML 解析,但执行可能阻塞 | defer:不阻塞;懒加载:完全不阻塞 |
执行顺序 | 无序 | defer:有序;懒加载:按需加载 |
适用场景 | 独立脚本(如统计代码) | 依赖 DOM 的脚本、非关键资源 |
四、最佳实践
1. JavaScript 加载策略
关键脚本:使用
defer
,确保在 DOM 就绪后按顺序执行。非关键脚本:使用
async
或动态加载(document.createElement('script')
)。避免无属性的
<script>
:同步加载会阻塞渲染。
2. 资源懒加载
图片/视频:优先使用
loading="lazy"
,兼容性不足时回退到 Intersection Observer。占位符:使用低质量占位图(LQIP)或骨架屏提升用户体验。
预加载关键资源:使用
<link rel="preload">
提前加载首屏内容。
3. 性能监控
使用 Lighthouse 或 WebPageTest 检测资源加载问题。
通过 Chrome DevTools 的
Network
面板分析加载时序。
五、示例代码
1. 异步加载 JavaScript
<!-- 异步加载独立脚本 -->
<script src="analytics.js" async></script>
2. 延迟加载 JavaScript
<!-- 延迟执行,保持顺序 -->
<script src="vendor.js" defer></script>
<script src="app.js" defer></script>
3. 图片懒加载
<!-- 使用 loading="lazy" -->
<img src="placeholder.jpg" data-src="image.jpg" loading="lazy" alt="Lazy Image">
<!-- 使用 Intersection Observer -->
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
</script>
总结
异步加载:通过
async
属性实现脚本并行加载和无序执行,适合独立脚本。延迟加载:
defer
:脚本延迟到 HTML 解析后执行,保持顺序。懒加载:延迟非关键资源(如图片)的加载,提升首屏性能。
合理结合这两种技术,可以显著优化页面加载速度、减少资源浪费,并提升用户体验。
Promise的构造函数
Promise
是 JavaScript 中用于处理异步操作的对象。它的构造函数用于创建一个新的 Promise
实例,并接受一个执行器函数(executor function)作为参数。以下是 Promise
构造函数的详细解析和使用方法。
1. Promise 构造函数的基本语法
new Promise(executor);
executor
:一个函数,接受两个参数resolve
和reject
,分别用于将Promise
的状态从 pending(等待) 转换为 fulfilled(已成功) 或 rejected(已失败)。
2. 参数说明
(1) resolve
函数
- 作用:将
Promise
的状态从 pending 转换为 fulfilled,并传递结果值。
resolve(value);
value
:传递给then
方法的成功回调的值。
(2) reject
函数
作用:将
Promise
的状态从 pending 转换为 rejected,并传递错误原因。语法:
reject(reason);
reason
:传递给catch
方法或then
的失败回调的错误原因。
3. 使用示例
(1) 基本用法
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation succeeded!');
} else {
reject('Operation failed!');
}
}, 1000);
});
promise
.then((result) => {
console.log(result); // 输出: "Operation succeeded!"
})
.catch((error) => {
console.error(error); // 输出: "Operation failed!"
});
(2) 链式调用
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
})
.then((result) => {
console.log(result); // 输出: 1
return result * 2;
})
.then((result) => {
console.log(result); // 输出: 2
return result * 2;
})
.then((result) => {
console.log(result); // 输出: 4
})
.catch((error) => {
console.error(error);
});
(3) 错误处理
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Something went wrong!')), 1000);
})
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error.message); // 输出: "Something went wrong!"
});
4. Promise 的状态
pending(等待):初始状态,既不是成功也不是失败。
fulfilled(已成功):操作成功完成,调用
resolve
。rejected(已失败):操作失败,调用
reject
。
5. 注意事项
立即执行:
executor
函数在Promise
构造函数调用时立即执行。不可逆:
Promise
的状态一旦从pending
转换为fulfilled
或rejected
,就不能再改变。错误处理:未捕获的
reject
会导致未处理的Promise
错误,建议始终使用.catch
或try/catch
(在async/await
中)。
总结
Promise
构造函数:用于创建Promise
实例,接受一个executor
函数。resolve
和reject
:用于改变Promise
的状态并传递结果或错误。链式调用:通过
.then
和.catch
处理成功和失败的结果。
Promise
是处理异步操作的核心工具,结合 async/await
可以编写更简洁、易读的异步代码。
同步和异步的区别
同步(Synchronous)和异步(Asynchronous)是编程中两种不同的执行模式,主要区别在于任务执行的顺序和阻塞行为。以下是它们的详细对比:
1. 定义
同步:
任务按顺序执行,前一个任务完成后才能执行下一个任务。
执行过程中会阻塞后续代码,直到当前任务完成。
异步:
任务可以并发执行,不需要等待前一个任务完成。
执行过程中不会阻塞后续代码,任务完成后通过回调、Promise 或事件通知。
2. 执行顺序
同步:
- 代码从上到下依次执行。
console.log("Task 1");
console.log("Task 2");
console.log("Task 3");
输出顺序:
Task 1
→Task 2
→Task 3
。异步:
- 代码执行顺序不确定,取决于任务完成时间。
console.log("Task 1");
setTimeout(() => console.log("Task 2"), 1000);
console.log("Task 3");
- 输出顺序:
Task 1
→Task 3
→Task 2
。
3. 阻塞行为
同步:
- 任务执行时会阻塞后续代码,直到任务完成。
console.log("Start");
for (let i = 0; i < 1e9; i++) {} // 长时间运行的任务
console.log("End");
在循环完成之前,
End
不会输出。异步:
- 任务执行时不会阻塞后续代码。
console.log("Start");
setTimeout(() => console.log("Task"), 1000);
console.log("End");
- 输出顺序:
Start
→End
→Task
。
4. 适用场景
同步:
适用于简单的、顺序执行的任务。
示例:文件读取、数学计算。
异步:
适用于耗时的、需要并发执行的任务。
示例:网络请求、文件读写、定时任务。
5. 实现方式
同步:
- 直接调用函数或方法。
const result = someSyncFunction();
console.log(result);
异步:
使用回调函数、Promise、async/await 等方式。
- 回调函数:
someAsyncFunction((err, result) => {
if (err) console.error(err);
else console.log(result);
});
- Promise:
someAsyncFunction()
.then(result => console.log(result))
.catch(err => console.error(err));
- async/await:
(async () => {
try {
const result = await someAsyncFunction();
console.log(result);
} catch (err) {
console.error(err);
}
})();
6. 性能
同步:
- 简单直接,但可能因阻塞导致性能问题。
异步:
- 提高资源利用率,适合处理高并发任务。
7. 错误处理
同步:
- 使用
try-catch
捕获错误。
- 使用
try {
const result = someSyncFunction();
console.log(result);
} catch (err) {
console.error(err);
}
异步:
使用回调函数的错误参数、Promise 的
catch
或try-catch
(配合 async/await)。- 回调函数:
someAsyncFunction((err, result) => {
if (err) console.error(err);
else console.log(result);
});
- Promise:
someAsyncFunction()
.then(result => console.log(result))
.catch(err => console.error(err));
- async/await:
(async () => {
try {
const result = await someAsyncFunction();
console.log(result);
} catch (err) {
console.error(err);
}
})();
总结
特性 | 同步(Synchronous) | 异步(Asynchronous) |
---|---|---|
执行顺序 | 顺序执行 | 并发执行 |
阻塞行为 | 阻塞后续代码 | 不阻塞后续代码 |
适用场景 | 简单、顺序任务 | 耗时、并发任务 |
实现方式 | 直接调用 | 回调、Promise、async/await |
性能 | 可能因阻塞导致性能问题 | 提高资源利用率 |
错误处理 | try-catch | 回调错误参数、catch、try-catch |
根据任务需求选择合适的执行模式,可以提高代码的效率和可维护性。
什么是设计模式,常见的设计模式有哪些
设计模式(Design Pattern)是软件开发中针对常见问题的可重用解决方案。它们不是具体的代码,而是经过验证的最佳实践和设计思想,帮助开发者构建更灵活、可维护和可扩展的软件系统。
设计模式最早由 Erich Gamma 等四位作者在《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书中提出,书中总结了23种经典的设计模式。
设计模式的分类
设计模式通常分为三大类:
创建型模式(Creational Patterns)
- 关注对象的创建机制,帮助系统独立于对象的创建、组合和表示。
- 常见模式:
- 单例模式(Singleton):确保一个类只有一个实例,并提供全局访问点。
- 工厂方法模式(Factory Method):定义一个创建对象的接口,但由子类决定实例化哪个类。
- 抽象工厂模式(Abstract Factory):提供一个创建一系列相关或依赖对象的接口,而无需指定具体类。
- 建造者模式(Builder):将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
- 原型模式(Prototype):通过复制现有对象来创建新对象,而不是通过实例化。
结构型模式(Structural Patterns)
- 关注类和对象的组合,形成更大的结构。
- 常见模式:
- 适配器模式(Adapter):将一个类的接口转换成客户端期望的另一个接口。
- 装饰器模式(Decorator):动态地为对象添加额外的职责,而不改变其结构。
- 代理模式(Proxy):为其他对象提供一个代理,以控制对这个对象的访问。
- 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。
- 外观模式(Facade):提供一个统一的接口,用来访问子系统中的一群接口。
- 桥接模式(Bridge):将抽象部分与实现部分分离,使它们可以独立变化。
- 享元模式(Flyweight):通过共享技术有效地支持大量细粒度对象。
行为型模式(Behavioral Patterns)
- 关注对象之间的职责分配和通信。
- 常见模式:
- 观察者模式(Observer):定义对象间的一对多依赖关系,当一个对象改变状态时,所有依赖者都会收到通知。
- 策略模式(Strategy):定义一系列算法,将它们封装起来,并使它们可以互相替换。
- 命令模式(Command):将请求封装为对象,从而使你可以用不同的请求对客户进行参数化。
- 状态模式(State):允许对象在其内部状态改变时改变其行为。
- 责任链模式(Chain of Responsibility):将请求的发送者和接收者解耦,使多个对象都有机会处理请求。
- 模板方法模式(Template Method):定义一个算法的骨架,将一些步骤延迟到子类中实现。
- 迭代器模式(Iterator):提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部表示。
- 中介者模式(Mediator):定义一个中介对象来封装一系列对象之间的交互。
- 备忘录模式(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
设计模式的核心思想
- 解耦:降低模块之间的依赖,提高系统的灵活性和可维护性。
- 复用:通过模式化的设计,避免重复造轮子。
- 可扩展性:使系统更容易适应变化。
- 清晰性:通过模式化的设计,使代码更易于理解和沟通。
常见设计模式的应用场景
- 单例模式:用于日志记录、数据库连接池等需要唯一实例的场景。
- 工厂模式:用于创建复杂对象或需要动态选择子类的场景。
- 观察者模式:用于事件驱动系统,如GUI框架中的事件监听。
- 策略模式:用于需要动态切换算法的场景,如支付方式的选择。
- 适配器模式:用于整合不兼容的接口,如旧系统与新系统的对接。
设计模式的优缺点
优点:
- 提供成熟的解决方案,减少设计风险。
- 提高代码的可读性和可维护性。
- 促进团队之间的沟通(模式名称成为通用语言)。
缺点:
- 过度使用可能导致代码复杂化。
- 某些模式可能增加系统的抽象层次,降低性能。
总结
设计模式是软件开发中的重要工具,但并不是银弹。在实际开发中,应根据具体需求选择合适的模式,避免为了使用模式而过度设计。理解设计模式的核心思想比死记硬背模式本身更为重要。
什么是代理模式
代理模式(Proxy Pattern)
代理模式是一种结构型设计模式,它允许你提供一个代理对象来控制对另一个对象的访问。代理对象在客户端和目标对象之间起到中介作用,可以在不改变目标对象的情况下,增加额外的功能或控制访问。
代理模式的核心思想
控制访问:
- 代理对象可以控制客户端对目标对象的访问,例如延迟初始化、权限验证等。
增强功能:
- 代理对象可以在调用目标对象的方法前后添加额外的逻辑,例如日志记录、缓存、性能监控等。
解耦:
- 代理模式将客户端与目标对象解耦,客户端无需直接操作目标对象。
代理模式的组成部分
Subject(抽象主题):
- 定义目标对象和代理对象的共同接口,客户端通过该接口与目标对象交互。
RealSubject(真实主题):
- 实际的目标对象,代理对象最终会调用它的方法。
Proxy(代理):
- 代理对象,持有对真实主题的引用,并控制对它的访问。
代理模式的实现方式
- 保护代理
保护代理用于控制对目标对象的访问权限。
class RealSubject {
request() {
console.log("RealSubject: Handling request.");
}
}
class Proxy {
constructor(realSubject) {
this.realSubject = realSubject;
}
request() {
if (this.checkAccess()) {
this.realSubject.request();
} else {
console.log("Proxy: Access denied.");
}
}
checkAccess() {
// 模拟权限检查
console.log("Proxy: Checking access.");
return true;
}
}
// 使用
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);
proxy.request();
输出:
Proxy: Checking access.
RealSubject: Handling request.
- 虚拟代理
虚拟代理用于延迟目标对象的初始化,直到真正需要时才创建。
class RealSubject {
request() {
console.log("RealSubject: Handling request.");
}
}
class Proxy {
constructor() {
this.realSubject = null;
}
request() {
if (!this.realSubject) {
this.realSubject = new RealSubject();
}
this.realSubject.request();
}
}
// 使用
const proxy = new Proxy();
proxy.request(); // 只有在调用时才会创建 RealSubject
输出:
RealSubject: Handling request.
- 缓存代理
缓存代理用于缓存目标对象的结果,避免重复计算。
class RealSubject {
heavyCompute(value) {
console.log("RealSubject: Computing...");
return value * 2;
}
}
class Proxy {
constructor(realSubject) {
this.realSubject = realSubject;
this.cache = new Map();
}
heavyCompute(value) {
if (this.cache.has(value)) {
console.log("Proxy: Returning cached result.");
return this.cache.get(value);
}
const result = this.realSubject.heavyCompute(value);
this.cache.set(value, result);
return result;
}
}
// 使用
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);
console.log(proxy.heavyCompute(10)); // 第一次计算
console.log(proxy.heavyCompute(10)); // 返回缓存结果
输出:
RealSubject: Computing...
20
Proxy: Returning cached result.
20
- 远程代理
远程代理用于隐藏目标对象在网络中的位置,客户端通过代理对象访问远程服务。
// 模拟远程服务
class RemoteService {
request() {
console.log("RemoteService: Handling request.");
}
}
// 远程代理
class Proxy {
request() {
console.log("Proxy: Forwarding request to remote service.");
const remoteService = new RemoteService();
remoteService.request();
}
}
// 使用
const proxy = new Proxy();
proxy.request();
输出:
Proxy: Forwarding request to remote service.
RemoteService: Handling request.
代理模式的优点
控制访问:
- 代理对象可以控制对目标对象的访问,例如权限验证、延迟初始化等。
增强功能:
- 代理对象可以在不修改目标对象的情况下,增加额外的功能,例如日志记录、缓存等。
解耦:
- 代理模式将客户端与目标对象解耦,客户端无需直接操作目标对象。
灵活性:
- 代理模式可以根据需要动态地创建和替换代理对象。
代理模式的缺点
复杂性增加:
- 引入代理对象会增加系统的复杂性,尤其是在多层代理的情况下。
性能开销:
- 代理对象可能会引入额外的性能开销,例如延迟初始化、远程调用等。
代码冗余:
- 代理对象需要实现与目标对象相同的接口,可能导致代码冗余。
代理模式的应用场景
延迟初始化:
- 当目标对象的创建成本较高时,可以使用虚拟代理延迟初始化。
访问控制:
- 当需要对目标对象的访问进行权限控制时,可以使用保护代理。
缓存:
- 当需要对目标对象的结果进行缓存时,可以使用缓存代理。
远程调用:
- 当目标对象位于远程服务器时,可以使用远程代理隐藏网络细节。
日志记录:
- 当需要在调用目标对象的方法前后记录日志时,可以使用代理模式。
总结
代理模式是一种强大的设计模式,适用于需要控制访问、增强功能或解耦的场景。通过代理对象,可以在不修改目标对象的情况下,灵活地扩展其功能。然而,代理模式也可能增加系统的复杂性和性能开销,因此在实际开发中应谨慎使用。
原型模式和单例模式的区别
原型模式(Prototype Pattern) 和 单例模式(Singleton Pattern) 是两种不同的设计模式,它们解决的问题和应用场景完全不同。以下是它们的详细对比:
- 定义与核心思想
单例模式
- 定义:确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
- 核心思想:限制类的实例化次数,确保全局唯一性。
- 应用场景:适用于需要全局唯一对象的场景,如配置管理、日志记录、数据库连接池等。
原型模式
- 定义:通过复制现有对象来创建新对象,而不是通过实例化类。
- 核心思想:利用对象的克隆能力,避免重复创建相似对象。
- 应用场景:适用于创建成本较高的对象,或需要动态生成对象的场景,如游戏中的敌人生成、复杂对象的复制等。
- 实现方式
单例模式
- 通过控制构造函数和静态方法实现。
- 示例:javascript
class Singleton { constructor() { if (Singleton.instance) { return Singleton.instance; } Singleton.instance = this; } } const instance1 = new Singleton(); const instance2 = new Singleton(); console.log(instance1 === instance2); // true
原型模式
- 通过 JavaScript 的原型链或
Object.create()
实现。 - 示例:javascript
const prototype = { greet() { console.log(`Hello, my name is ${this.name}`); } }; const obj1 = Object.create(prototype); obj1.name = "Alice"; obj1.greet(); // Hello, my name is Alice const obj2 = Object.create(prototype); obj2.name = "Bob"; obj2.greet(); // Hello, my name is Bob
- 主要区别
特性 | 单例模式 | 原型模式 |
---|---|---|
目的 | 确保一个类只有一个实例 | 通过复制现有对象创建新对象 |
实例数量 | 只有一个实例 | 可以创建多个实例 |
创建方式 | 通过构造函数或静态方法创建 | 通过克隆现有对象创建 |
适用场景 | 全局唯一对象(如配置、日志) | 创建成本高的对象或需要动态生成对象的场景 |
灵活性 | 灵活性较低,只能有一个实例 | 灵活性较高,可以动态生成多个对象 |
性能 | 适合频繁访问的场景 | 适合避免重复创建复杂对象的场景 |
代码复杂度 | 实现简单 | 实现稍复杂,需要理解原型链或克隆机制 |
- 应用场景对比
单例模式的应用场景
- 配置管理:
- 应用程序的配置信息通常只需要一个全局实例。
- 日志记录:
- 日志记录器通常只需要一个实例来统一管理日志输出。
- 数据库连接池:
- 数据库连接池通常只需要一个实例来管理连接。
- 状态管理:
- 在 Redux 或 Vuex 等状态管理库中,store 通常是单例的。
原型模式的应用场景
游戏开发:
- 游戏中的敌人、道具等对象可以通过原型模式快速生成。
复杂对象复制:
- 当对象的创建成本较高时,可以通过克隆避免重复初始化。
动态对象生成:
- 在运行时根据需求动态生成对象。
缓存对象:
- 通过克隆缓存的对象,减少重复创建的开销。
代码示例对比
单例模式
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
Logger.instance = this;
}
log(message) {
this.logs.push(message);
console.log(`Log: ${message}`);
}
}
const logger1 = new Logger();
logger1.log("First log"); // Log: First log
const logger2 = new Logger();
logger2.log("Second log"); // Log: Second log
console.log(logger1 === logger2); // true
原型模式
const enemyPrototype = {
attack() {
console.log(`${this.name} attacks!`);
},
clone(name) {
const clone = Object.create(this);
clone.name = name;
return clone;
}
};
const enemy1 = enemyPrototype.clone("Goblin");
enemy1.attack(); // Goblin attacks!
const enemy2 = enemyPrototype.clone("Orc");
enemy2.attack(); // Orc attacks!
console.log(enemy1 === enemy2); // false
总结
单例模式:
- 强调全局唯一性,适合需要共享资源的场景。
- 实现简单,但灵活性较低。
原型模式:
- 强调对象的复制和动态生成,适合创建成本高的对象。
- 实现稍复杂,但灵活性较高。
在实际开发中,应根据具体需求选择合适的设计模式。如果需要全局唯一对象,使用单例模式;如果需要动态生成或复制对象,使用原型模式。
组合模式的适用性是什么,什么时候使用组合模式
组合模式(Composite Pattern)
组合模式是一种结构型设计模式,它允许你将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端可以统一对待单个对象和组合对象,从而简化代码逻辑。
组合模式的核心思想
统一对待:
- 组合模式使得客户端可以一致地处理单个对象和组合对象,无需区分它们。
树形结构:
- 组合模式通过树形结构表示对象的层次关系,适合处理具有递归性质的数据。
简化代码:
- 通过组合模式,可以避免编写大量的条件判断代码来处理单个对象和组合对象。
组合模式的组成部分
Component(组件):
- 定义所有对象的通用接口,包括叶子节点和组合节点。
Leaf(叶子节点):
- 表示树形结构中的叶子节点,没有子节点。
Composite(组合节点):
- 表示树形结构中的组合节点,可以包含子节点(叶子节点或其他组合节点)。
组合模式的适用性
组合模式适用于以下场景:
部分-整体层次结构:
- 当需要表示对象的“部分-整体”层次结构时,可以使用组合模式。例如,文件系统中的文件和文件夹。
统一处理:
- 当希望客户端可以统一处理单个对象和组合对象时,可以使用组合模式。例如,图形编辑器中的图形和图形组。
递归结构:
- 当数据结构具有递归性质时,可以使用组合模式。例如,组织架构中的部门和员工。
简化代码:
- 当需要避免编写大量的条件判断代码来处理单个对象和组合对象时,可以使用组合模式。
组合模式的实现
以下是一个文件系统的示例,展示如何使用组合模式表示文件和文件夹的层次结构。
// Component
class FileSystemComponent {
constructor(name) {
this.name = name;
}
display() {
throw new Error("This method must be overridden.");
}
}
// Leaf
class File extends FileSystemComponent {
display() {
console.log(`File: ${this.name}`);
}
}
// Composite
class Folder extends FileSystemComponent {
constructor(name) {
super(name);
this.children = [];
}
add(component) {
this.children.push(component);
}
remove(component) {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
display() {
console.log(`Folder: ${this.name}`);
this.children.forEach(child => child.display());
}
}
// 使用
const root = new Folder("Root");
const folder1 = new Folder("Folder 1");
const folder2 = new Folder("Folder 2");
const file1 = new File("File 1");
const file2 = new File("File 2");
const file3 = new File("File 3");
folder1.add(file1);
folder1.add(file2);
folder2.add(file3);
root.add(folder1);
root.add(folder2);
root.display();
输出:
Folder: Root
Folder: Folder 1
File: File 1
File: File 2
Folder: Folder 2
File: File 3
组合模式的优点
简化客户端代码:
- 客户端可以一致地处理单个对象和组合对象,无需区分它们。
灵活性:
- 可以轻松地添加新的组件类型,扩展性强。
清晰的层次结构:
- 组合模式通过树形结构清晰地表示对象的层次关系。
组合模式的缺点
设计复杂性:
- 组合模式的设计可能较为复杂,尤其是需要处理递归结构时。
性能开销:
- 在处理深层嵌套的组合结构时,可能会引入性能开销。
类型检查:
- 在某些情况下,客户端可能仍然需要检查对象的类型,从而破坏了组合模式的统一性。
组合模式的应用场景
文件系统:
- 文件和文件夹的层次结构是组合模式的经典应用场景。
图形编辑器:
- 图形和图形组的层次结构可以使用组合模式表示。
组织架构:
- 部门和员工的层次结构可以使用组合模式表示。
UI组件:
- UI框架中的容器组件和叶子组件可以使用组合模式表示。
菜单系统:
- 菜单和菜单项的层次结构可以使用组合模式表示。
总结
组合模式是一种强大的设计模式,适用于表示“部分-整体”的层次结构。它使得客户端可以统一处理单个对象和组合对象,从而简化代码逻辑。然而,组合模式的设计可能较为复杂,且在处理深层嵌套结构时可能会引入性能开销。在实际开发中,应根据具体需求选择是否使用组合模式。
什么是工厂模式,有什么优点
工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,而无需指定具体的类。工厂模式通过一个工厂方法或工厂类来封装对象的创建逻辑,使得客户端代码与具体类的实现解耦。
1. 工厂模式的类型
简单工厂模式:
- 通过一个工厂方法根据输入参数创建不同的对象。
工厂方法模式:
- 定义一个创建对象的接口,但由子类决定实例化哪个类。
抽象工厂模式:
- 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。
2. 简单工厂模式示例
class Car {
constructor(name) {
this.name = name;
}
drive() {
console.log(`${this.name} is driving.`);
}
}
class Truck {
constructor(name) {
this.name = name;
}
drive() {
console.log(`${this.name} is driving.`);
}
}
class VehicleFactory {
static createVehicle(type, name) {
switch (type) {
case 'car':
return new Car(name);
case 'truck':
return new Truck(name);
default:
throw new Error('Unknown vehicle type');
}
}
}
const car = VehicleFactory.createVehicle('car', 'Tesla');
car.drive(); // 输出: Tesla is driving.
const truck = VehicleFactory.createVehicle('truck', 'Ford');
truck.drive(); // 输出: Ford is driving.
3. 工厂方法模式示例
class Vehicle {
drive() {
console.log(`${this.name} is driving.`);
}
}
class Car extends Vehicle {
constructor(name) {
super();
this.name = name;
}
}
class Truck extends Vehicle {
constructor(name) {
super();
this.name = name;
}
}
class VehicleFactory {
createVehicle(name) {
throw new Error('This method must be overridden');
}
}
class CarFactory extends VehicleFactory {
createVehicle(name) {
return new Car(name);
}
}
class TruckFactory extends VehicleFactory {
createVehicle(name) {
return new Truck(name);
}
}
const carFactory = new CarFactory();
const car = carFactory.createVehicle('Tesla');
car.drive(); // 输出: Tesla is driving.
const truckFactory = new TruckFactory();
const truck = truckFactory.createVehicle('Ford');
truck.drive(); // 输出: Ford is driving.
4. 抽象工厂模式示例
class Car {
drive() {
console.log('Car is driving.');
}
}
class Truck {
drive() {
console.log('Truck is driving.');
}
}
class VehicleFactory {
createCar() {
throw new Error('This method must be overridden');
}
createTruck() {
throw new Error('This method must be overridden');
}
}
class ConcreteVehicleFactory extends VehicleFactory {
createCar() {
return new Car();
}
createTruck() {
return new Truck();
}
}
const factory = new ConcreteVehicleFactory();
const car = factory.createCar();
car.drive(); // 输出: Car is driving.
const truck = factory.createTruck();
truck.drive(); // 输出: Truck is driving.
5. 工厂模式的优点
解耦:
- 将对象的创建与使用分离,客户端代码无需关心具体类的实现。
可扩展性:
- 添加新的产品类型时,只需扩展工厂类,无需修改现有代码。
代码复用:
- 将对象的创建逻辑集中管理,避免重复代码。
易于测试:
- 可以通过模拟工厂类来测试客户端代码。
总结
类型 | 描述 | 优点 |
---|---|---|
简单工厂模式 | 通过一个工厂方法创建对象 | 简单易用,适合简单场景 |
工厂方法模式 | 由子类决定创建的对象类型 | 符合开闭原则,易于扩展 |
抽象工厂模式 | 创建一系列相关或相互依赖的对象 | 适合复杂场景,支持产品族 |
工厂模式是一种强大的设计模式,合理使用可以提高代码的灵活性和可维护性。
JavaScript单例模式及其优缺点
JavaScript 单例模式
单例模式(Singleton Pattern) 是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。单例模式通常用于管理全局状态、共享资源或限制某些对象的实例化次数。
实现单例模式的几种方式
- 使用对象字面量
最简单的单例模式实现方式是直接使用对象字面量。
const singleton = {
property: "value",
method() {
console.log("I am a method");
}
};
// 使用
singleton.method(); // 输出: I am a method
优点:
- 简单直接,无需复杂的逻辑。
- 天然单例,因为对象字面量本身就是唯一的。
缺点:
- 无法延迟初始化(即无法在需要时才创建实例)。
- 无法通过构造函数传递参数。
- 使用闭包
通过闭包实现单例模式,可以隐藏实例并控制其创建时机。
const Singleton = (function () {
let instance;
function createInstance() {
const object = new Object("I am the instance");
return object;
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // 输出: true
优点:
- 延迟初始化,实例在第一次调用时创建。
- 隐藏实例化逻辑,外部无法直接访问。
缺点:
- 代码稍复杂,需要理解闭包的概念。
- 使用 ES6 类
通过 ES6 类和静态方法实现单例模式。
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
this.property = "value";
}
method() {
console.log("I am a method");
}
}
// 使用
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // 输出: true
instance1.method(); // 输出: I am a method
优点:
- 符合现代 JavaScript 语法,易于理解。
- 支持延迟初始化。
缺点:
- 需要手动管理实例,容易出错。
- 使用模块化
在现代 JavaScript 中,模块化本身就是一种天然的单例模式。
// singleton.js
let instance;
export default class Singleton {
constructor() {
if (instance) {
return instance;
}
instance = this;
this.property = "value";
}
method() {
console.log("I am a method");
}
}
// 使用
import Singleton from './singleton.js';
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // 输出: true
优点:
- 模块化天然支持单例。
- 代码清晰,易于维护。
缺点:
- 依赖于模块系统(如 ES Modules)。
单例模式的优点
全局访问点:
- 单例模式提供了一个全局访问点,方便在应用程序的任何地方访问实例。
节省资源:
- 单例模式确保只有一个实例存在,避免了重复创建对象的开销。
延迟初始化:
- 单例模式可以延迟实例化,只有在需要时才创建实例。
共享状态:
- 单例模式适合管理全局状态或共享资源(如配置、缓存、数据库连接池等)。
单例模式的缺点
全局状态污染:
- 单例模式本质上是一个全局变量,可能导致全局状态污染,增加代码的耦合性。
难以测试:
- 单例模式的全局性使得单元测试变得困难,因为测试用例之间可能会相互影响。
违反单一职责原则:
- 单例类通常既负责业务逻辑,又负责管理自己的生命周期,违反了单一职责原则。
难以扩展:
- 单例模式的硬编码特性使得它难以扩展或修改。
单例模式的应用场景
配置管理:
- 应用程序的配置信息通常只需要一个实例。
缓存系统:
- 缓存对象通常只需要一个全局实例。
日志记录:
- 日志记录器通常只需要一个实例来统一管理日志输出。
数据库连接池:
- 数据库连接池通常只需要一个实例来管理连接。
状态管理:
- 在 Redux 或 Vuex 等状态管理库中,store 通常是单例的。
总结
单例模式是一种简单但强大的设计模式,适用于需要全局唯一实例的场景。然而,它也有一些缺点,如全局状态污染和难以测试。在实际开发中,应谨慎使用单例模式,确保其带来的好处大于潜在的缺点。
JavaScript抽象工厂模式
JavaScript 抽象工厂模式
抽象工厂模式(Abstract Factory Pattern) 是一种创建型设计模式,它提供了一种创建一系列相关或依赖对象的接口,而无需指定它们的具体类。抽象工厂模式的核心思想是将对象的创建与使用分离,使得系统可以在不修改客户端代码的情况下切换整个产品族。
抽象工厂模式的核心思想
产品族:
- 抽象工厂模式关注的是创建一组相关的对象(称为产品族),而不是单个对象。
解耦:
- 客户端代码只依赖于抽象工厂和抽象产品接口,与具体实现解耦。
可扩展性:
- 可以通过添加新的具体工厂来支持新的产品族,而无需修改现有代码。
抽象工厂模式的组成部分
AbstractFactory(抽象工厂):
- 定义创建一系列产品对象的接口。
ConcreteFactory(具体工厂):
- 实现抽象工厂接口,创建具体的产品对象。
AbstractProduct(抽象产品):
- 定义产品对象的接口。
ConcreteProduct(具体产品):
- 实现抽象产品接口,是具体工厂创建的对象。
抽象工厂模式的实现
以下是一个简单的示例,展示如何使用抽象工厂模式创建不同主题的 UI 组件(按钮和文本框)。
- 定义抽象产品
// 抽象产品:按钮
class Button {
render() {
throw new Error("This method must be overridden.");
}
}
// 抽象产品:文本框
class TextBox {
render() {
throw new Error("This method must be overridden.");
}
}
- 定义具体产品
// 具体产品:Light 主题按钮
class LightButton extends Button {
render() {
console.log("Rendering a light theme button.");
}
}
// 具体产品:Light 主题文本框
class LightTextBox extends TextBox {
render() {
console.log("Rendering a light theme text box.");
}
}
// 具体产品:Dark 主题按钮
class DarkButton extends Button {
render() {
console.log("Rendering a dark theme button.");
}
}
// 具体产品:Dark 主题文本框
class DarkTextBox extends TextBox {
render() {
console.log("Rendering a dark theme text box.");
}
}
- 定义抽象工厂
// 抽象工厂
class ThemeFactory {
createButton() {
throw new Error("This method must be overridden.");
}
createTextBox() {
throw new Error("This method must be overridden.");
}
}
- 定义具体工厂
// 具体工厂:Light 主题工厂
class LightThemeFactory extends ThemeFactory {
createButton() {
return new LightButton();
}
createTextBox() {
return new LightTextBox();
}
}
// 具体工厂:Dark 主题工厂
class DarkThemeFactory extends ThemeFactory {
createButton() {
return new DarkButton();
}
createTextBox() {
return new DarkTextBox();
}
}
- 使用抽象工厂
// 客户端代码
function renderUI(factory) {
const button = factory.createButton();
const textBox = factory.createTextBox();
button.render();
textBox.render();
}
// 使用 Light 主题
const lightFactory = new LightThemeFactory();
renderUI(lightFactory);
// 使用 Dark 主题
const darkFactory = new DarkThemeFactory();
renderUI(darkFactory);
输出:
Rendering a light theme button.
Rendering a light theme text box.
Rendering a dark theme button.
Rendering a dark theme text box.
抽象工厂模式的优点
解耦:
- 客户端代码只依赖于抽象工厂和抽象产品接口,与具体实现解耦。
一致性:
- 抽象工厂模式确保创建的对象属于同一产品族,保持一致性。
可扩展性:
- 可以通过添加新的具体工厂来支持新的产品族,而无需修改现有代码。
抽象工厂模式的缺点
复杂性:
- 抽象工厂模式引入了更多的类和接口,增加了系统的复杂性。
灵活性:
- 如果需要支持新的产品类型,需要修改抽象工厂接口及其所有具体工厂。
抽象工厂模式的应用场景
跨平台 UI 框架:
- 在不同操作系统(如 Windows、macOS)上创建一致的 UI 组件。
主题系统:
- 在应用程序中支持多种主题(如 Light 主题、Dark 主题)。
游戏开发:
- 创建不同风格的游戏角色、道具和场景。
数据库访问:
- 支持多种数据库(如 MySQL、PostgreSQL)的访问接口。
总结
抽象工厂模式是一种强大的设计模式,适用于创建一系列相关或依赖对象的场景。它通过将对象的创建与使用分离,提高了系统的灵活性和可扩展性。然而,抽象工厂模式也可能增加系统的复杂性,因此在设计时需要权衡其优缺点。
JavaScript原型模式
JavaScript 原型模式
原型模式(Prototype Pattern) 是一种创建型设计模式,它通过复制现有对象来创建新对象,而不是通过实例化类。原型模式的核心思想是利用对象的克隆能力,避免重复创建相似对象。
原型模式的核心思想
克隆对象:
- 通过复制现有对象来创建新对象,而不是通过构造函数。
减少开销:
- 避免重复创建相似对象,尤其是当对象的创建成本较高时。
动态生成:
- 在运行时根据需要动态生成对象。
原型模式的实现
在 JavaScript 中,原型模式可以通过 Object.create()
或直接复制对象来实现。
- 使用
Object.create()
Object.create()
方法创建一个新对象,并使用现有对象作为新对象的原型。
// 原型对象
const prototype = {
greet() {
console.log(`Hello, my name is ${this.name}`);
}
};
// 创建新对象并继承原型
const obj1 = Object.create(prototype);
obj1.name = "Alice";
obj1.greet(); // 输出: Hello, my name is Alice
const obj2 = Object.create(prototype);
obj2.name = "Bob";
obj2.greet(); // 输出: Hello, my name is Bob
- 使用构造函数和原型链
通过构造函数和原型链实现原型模式。
// 构造函数
function Person(name) {
this.name = name;
}
// 在原型上定义方法
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
// 创建新对象
const person1 = new Person("Alice");
person1.greet(); // 输出: Hello, my name is Alice
const person2 = new Person("Bob");
person2.greet(); // 输出: Hello, my name is Bob
- 使用
Object.assign()
Object.assign()
方法可以将多个对象的属性复制到目标对象中。
// 原型对象
const prototype = {
greet() {
console.log(`Hello, my name is ${this.name}`);
}
};
// 创建新对象并复制原型属性
const obj1 = Object.assign({}, prototype, { name: "Alice" });
obj1.greet(); // 输出: Hello, my name is Alice
const obj2 = Object.assign({}, prototype, { name: "Bob" });
obj2.greet(); // 输出: Hello, my name is Bob
原型模式的优点
减少开销:
- 通过克隆现有对象来创建新对象,避免重复创建相似对象。
动态生成:
- 在运行时根据需要动态生成对象。
简化对象创建:
- 避免复杂的构造函数调用,直接复制现有对象。
原型模式的缺点
深拷贝问题:
- 如果对象包含引用类型的属性,浅拷贝会导致新对象和原对象共享引用。
复杂性增加:
- 如果对象的层次结构较深,克隆过程可能会变得复杂。
不适合所有场景:
- 原型模式适合创建相似对象,但不适合创建差异较大的对象。
原型模式的应用场景
游戏开发:
- 游戏中的敌人、道具等对象可以通过原型模式快速生成。
复杂对象复制:
- 当对象的创建成本较高时,可以通过克隆避免重复初始化。
动态对象生成:
- 在运行时根据需求动态生成对象。
缓存对象:
- 通过克隆缓存的对象,减少重复创建的开销。
示例:游戏中的敌人生成
以下是一个游戏开发中的示例,展示如何使用原型模式生成敌人对象。
// 敌人原型
const enemyPrototype = {
health: 100,
attack() {
console.log(`${this.name} attacks!`);
},
clone(name) {
const clone = Object.create(this);
clone.name = name;
return clone;
}
};
// 创建敌人
const enemy1 = enemyPrototype.clone("Goblin");
enemy1.attack(); // 输出: Goblin attacks!
const enemy2 = enemyPrototype.clone("Orc");
enemy2.attack(); // 输出: Orc attacks!
console.log(enemy1 === enemy2); // 输出: false
总结
原型模式是一种简单但强大的设计模式,适用于通过克隆现有对象来创建新对象的场景。它通过减少对象创建的开销和动态生成对象,提高了代码的灵活性和性能。然而,原型模式也可能引入深拷贝问题和复杂性,因此在设计时需要根据具体需求进行权衡。
JavaScript中介者模式
JavaScript 中介者模式
中介者模式(Mediator Pattern) 是一种行为型设计模式,它通过引入一个中介者对象来封装一系列对象之间的交互。中介者模式的核心思想是减少对象之间的直接通信,从而降低系统的耦合度。
中介者模式的核心思想
封装交互:
- 中介者对象封装了多个对象之间的交互逻辑,对象之间不再直接通信。
降低耦合:
- 对象只与中介者通信,减少了对象之间的依赖关系。
集中控制:
- 中介者集中管理对象之间的交互,使得系统更易于理解和维护。
中介者模式的组成部分
Mediator(中介者):
- 定义对象之间交互的接口。
ConcreteMediator(具体中介者):
- 实现中介者接口,协调各个对象之间的交互。
Colleague(同事类):
- 定义各个对象的接口,每个对象都知道中介者。
ConcreteColleague(具体同事类):
- 实现同事类接口,与其他对象通过中介者通信。
中介者模式的实现
以下是一个简单的示例,展示如何使用中介者模式实现聊天室中的用户通信。
- 定义中介者接口
// 中介者接口
class ChatRoomMediator {
sendMessage(message, user) {
throw new Error("This method must be overridden.");
}
}
- 定义具体中介者
// 具体中介者:聊天室
class ChatRoom extends ChatRoomMediator {
constructor() {
super();
this.users = [];
}
addUser(user) {
this.users.push(user);
}
sendMessage(message, sender) {
this.users.forEach(user => {
if (user !== sender) {
user.receive(message);
}
});
}
}
- 定义同事类
// 同事类:用户
class User {
constructor(name, mediator) {
this.name = name;
this.mediator = mediator;
}
send(message) {
console.log(`${this.name} sends: ${message}`);
this.mediator.sendMessage(message, this);
}
receive(message) {
console.log(`${this.name} receives: ${message}`);
}
}
- 使用中介者模式
// 创建中介者
const chatRoom = new ChatRoom();
// 创建用户
const user1 = new User("Alice", chatRoom);
const user2 = new User("Bob", chatRoom);
const user3 = new User("Charlie", chatRoom);
// 将用户添加到聊天室
chatRoom.addUser(user1);
chatRoom.addUser(user2);
chatRoom.addUser(user3);
// 用户发送消息
user1.send("Hello, everyone!");
user2.send("Hi, Alice!");
user3.send("Hey, Bob!");
输出:
Alice sends: Hello, everyone!
Bob receives: Hello, everyone!
Charlie receives: Hello, everyone!
Bob sends: Hi, Alice!
Alice receives: Hi, Alice!
Charlie receives: Hi, Alice!
Charlie sends: Hey, Bob!
Alice receives: Hey, Bob!
Bob receives: Hey, Bob!
中介者模式的优点
降低耦合:
- 对象之间不再直接通信,而是通过中介者交互,降低了耦合度。
集中控制:
- 中介者集中管理对象之间的交互,使得系统更易于理解和维护。
简化对象:
- 对象只需要知道中介者,而不需要知道其他对象,简化了对象的设计。
中介者模式的缺点
中介者复杂性:
- 中介者对象可能会变得复杂,尤其是当对象之间的交互逻辑较多时。
性能开销:
- 中介者模式可能会引入额外的性能开销,因为所有交互都需要通过中介者。
中介者模式的应用场景
聊天系统:
- 在聊天室中,用户之间的消息传递可以通过中介者管理。
GUI 框架:
- 在图形用户界面中,组件之间的交互可以通过中介者管理。
游戏开发:
- 在游戏中,角色、道具和场景之间的交互可以通过中介者管理。
工作流系统:
- 在工作流中,任务之间的依赖关系可以通过中介者管理。
总结
中介者模式是一种强大的设计模式,适用于需要减少对象之间直接通信的场景。它通过引入中介者对象来封装对象之间的交互,降低了系统的耦合度。然而,中介者模式也可能导致中介者对象变得复杂,因此在设计时需要根据具体需求进行权衡。
JavaScript装饰模式
JavaScript 装饰模式
装饰模式(Decorator Pattern) 是一种结构型设计模式,它允许你通过将对象放入包含行为的特殊封装对象中来为原对象动态添加新的行为。装饰模式的核心思想是通过组合而非继承来扩展对象的功能。
装饰模式的核心思想
动态扩展:
- 装饰模式允许你在运行时动态地为对象添加功能,而无需修改其原始类。
组合优于继承:
- 通过组合对象来扩展功能,避免了继承带来的类爆炸问题。
单一职责原则:
- 每个装饰类只负责一个特定的功能,符合单一职责原则。
装饰模式的组成部分
Component(组件):
- 定义对象的接口,可以是抽象类或接口。
ConcreteComponent(具体组件):
- 实现组件接口,是被装饰的原始对象。
Decorator(装饰器):
- 持有组件对象的引用,并实现组件接口。
ConcreteDecorator(具体装饰器):
- 实现装饰器接口,为组件添加新的行为。
装饰模式的实现
以下是一个简单的示例,展示如何使用装饰模式为咖啡添加不同的调料。
- 定义组件接口
// 组件接口:饮料
class Beverage {
getDescription() {
throw new Error("This method must be overridden.");
}
cost() {
throw new Error("This method must be overridden.");
}
}
- 定义具体组件
// 具体组件:咖啡
class Coffee extends Beverage {
getDescription() {
return "Coffee";
}
cost() {
return 5;
}
}
- 定义装饰器
// 装饰器:调料
class CondimentDecorator extends Beverage {
constructor(beverage) {
super();
this.beverage = beverage;
}
getDescription() {
return this.beverage.getDescription();
}
cost() {
return this.beverage.cost();
}
}
- 定义具体装饰器
// 具体装饰器:牛奶
class Milk extends CondimentDecorator {
getDescription() {
return `${this.beverage.getDescription()}, Milk`;
}
cost() {
return this.beverage.cost() + 2;
}
}
// 具体装饰器:糖
class Sugar extends CondimentDecorator {
getDescription() {
return `${this.beverage.getDescription()}, Sugar`;
}
cost() {
return this.beverage.cost() + 1;
}
}
- 使用装饰模式
// 创建原始对象:咖啡
let coffee = new Coffee();
console.log(coffee.getDescription(), coffee.cost()); // 输出: Coffee 5
// 添加牛奶
coffee = new Milk(coffee);
console.log(coffee.getDescription(), coffee.cost()); // 输出: Coffee, Milk 7
// 添加糖
coffee = new Sugar(coffee);
console.log(coffee.getDescription(), coffee.cost()); // 输出: Coffee, Milk, Sugar 8
装饰模式的优点
动态扩展:
- 可以在运行时动态地为对象添加功能,而无需修改其原始类。
单一职责原则:
- 每个装饰类只负责一个特定的功能,符合单一职责原则。
避免类爆炸:
- 通过组合对象来扩展功能,避免了继承带来的类爆炸问题。
装饰模式的缺点
复杂性增加:
- 装饰模式引入了大量的装饰类,可能会增加系统的复杂性。
调试困难:
- 由于对象被多层装饰,调试时可能会比较困难。
装饰模式的应用场景
动态添加功能:
- 当需要动态地为对象添加功能时,可以使用装饰模式。
避免子类膨胀:
- 当使用继承会导致子类数量爆炸时,可以使用装饰模式。
扩展第三方库:
- 当需要扩展第三方库的功能时,可以使用装饰模式。
总结
装饰模式是一种强大的设计模式,适用于需要动态扩展对象功能的场景。它通过组合对象来扩展功能,避免了继承带来的类爆炸问题。然而,装饰模式也可能增加系统的复杂性和调试难度,因此在设计时需要根据具体需求进行权衡。
JavaScript策略模式
JavaScript 策略模式
策略模式(Strategy Pattern) 是一种行为型设计模式,它允许你定义一系列算法,并将它们封装在独立的类中,使得它们可以互相替换。策略模式的核心思想是将算法的使用与算法的实现分离,从而提高代码的灵活性和可维护性。
策略模式的核心思想
封装算法:
- 将每个算法封装在独立的类中,使得它们可以互相替换。
解耦:
- 将算法的使用与算法的实现分离,降低代码的耦合度。
动态切换:
- 在运行时动态地切换算法,而无需修改客户端代码。
策略模式的组成部分
Context(上下文):
- 持有一个策略对象的引用,并提供一个接口来执行策略。
Strategy(策略):
- 定义所有支持的算法的公共接口。
ConcreteStrategy(具体策略):
- 实现策略接口,提供具体的算法实现。
策略模式的实现
以下是一个简单的示例,展示如何使用策略模式实现不同的支付方式。
- 定义策略接口
// 策略接口:支付方式
class PaymentStrategy {
pay(amount) {
throw new Error("This method must be overridden.");
}
}
- 定义具体策略
// 具体策略:信用卡支付
class CreditCardPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} using Credit Card.`);
}
}
// 具体策略:支付宝支付
class AlipayPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} using Alipay.`);
}
}
// 具体策略:微信支付
class WechatPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} using Wechat Pay.`);
}
}
- 定义上下文
// 上下文:购物车
class ShoppingCart {
constructor(paymentStrategy) {
this.paymentStrategy = paymentStrategy;
this.items = [];
}
addItem(item) {
this.items.push(item);
}
calculateTotal() {
return this.items.reduce((total, item) => total + item.price, 0);
}
checkout() {
const total = this.calculateTotal();
this.paymentStrategy.pay(total);
}
}
- 使用策略模式
// 创建购物车
const cart = new ShoppingCart(new CreditCardPayment());
// 添加商品
cart.addItem({ name: "Book", price: 50 });
cart.addItem({ name: "Phone", price: 1000 });
// 结账
cart.checkout(); // 输出: Paid 1050 using Credit Card.
// 切换支付方式
cart.paymentStrategy = new AlipayPayment();
cart.checkout(); // 输出: Paid 1050 using Alipay.
cart.paymentStrategy = new WechatPayment();
cart.checkout(); // 输出: Paid 1050 using Wechat Pay.
策略模式的优点
灵活性强:
- 可以在运行时动态地切换算法,而无需修改客户端代码。
解耦:
- 将算法的使用与算法的实现分离,降低代码的耦合度。
易于扩展:
- 可以通过添加新的策略类来扩展系统,而无需修改现有代码。
策略模式的缺点
策略类增多:
- 如果算法较多,可能会导致策略类的数量增加。
客户端需要了解策略:
- 客户端需要知道所有的策略类,并选择合适的策略。
策略模式的应用场景
支付方式:
- 在电商平台中,支持多种支付方式(如信用卡、支付宝、微信支付等)。
排序算法:
- 在需要支持多种排序算法(如快速排序、归并排序等)的场景。
验证规则:
- 在表单验证中,支持多种验证规则(如邮箱验证、手机号验证等)。
游戏开发:
- 在游戏中,支持不同的角色行为(如攻击、防御、逃跑等)。
总结
策略模式是一种强大的设计模式,适用于需要动态切换算法的场景。它通过将算法的使用与算法的实现分离,提高了代码的灵活性和可维护性。然而,策略模式也可能导致策略类的数量增加,因此在设计时需要根据具体需求进行权衡。
JavaScript中MC架构和MVVM架构
在JavaScript开发中,MC架构和MVVM架构是两种常见的设计模式,用于组织代码结构、分离关注点并提高应用的可维护性和可扩展性。以下是它们的详细说明和对比:
- MC架构(Model-Controller)
MC架构是一种简化版的MVC架构,去掉了View层,主要由Model和Controller组成。
组成部分:
Model(模型):
- 负责管理应用程序的数据和业务逻辑。
- 与后端服务器交互,获取或保存数据。
- 不直接处理用户界面。
Controller(控制器):
- 负责处理用户输入(如点击事件、表单提交等)。
- 从Model中获取数据,并决定如何更新UI。
- 在MC架构中,Controller通常直接操作DOM来更新视图。
特点:
- 简单直接:适合小型应用或快速原型开发。
- 紧耦合:Controller直接操作DOM,导致视图和逻辑耦合度较高。
- 缺乏清晰的视图层:视图逻辑分散在Controller中,难以维护。
示例:
// Model
class UserModel {
constructor() {
this.users = [];
}
fetchUsers() {
// 模拟从服务器获取数据
this.users = [{ name: 'Alice' }, { name: 'Bob' }];
}
}
// Controller
class UserController {
constructor(model) {
this.model = model;
}
render() {
const userList = document.getElementById('user-list');
userList.innerHTML = this.model.users.map(user => `<li>${user.name}</li>`).join('');
}
handleButtonClick() {
this.model.fetchUsers();
this.render();
}
}
// 初始化
const model = new UserModel();
const controller = new UserController(model);
document.getElementById('fetch-button').addEventListener('click', () => controller.handleButtonClick());
- MVVM架构(Model-View-ViewModel)
MVVM架构是一种更现代化的架构模式,广泛应用于前端框架(如Vue.js、Knockout.js等)。它将视图逻辑与业务逻辑进一步分离,通过数据绑定实现自动更新。
组成部分:
Model(模型):
- 负责管理应用程序的数据和业务逻辑。
- 与后端服务器交互,获取或保存数据。
View(视图):
- 负责展示数据,通常是HTML模板。
- 不直接处理业务逻辑。
ViewModel(视图模型):
- 是View和Model之间的桥梁。
- 负责将Model中的数据转换为View可以展示的形式。
- 通过数据绑定机制,自动将View与ViewModel同步。
特点:
- 数据驱动:通过数据绑定实现视图的自动更新。
- 解耦:View和Model完全分离,ViewModel负责协调。
- 适合复杂应用:适合需要频繁更新UI的大型应用。
示例(使用Vue.js):
<div id="app">
<ul>
<li v-for="user in users" :key="user.name">{{ user.name }}</li>
</ul>
<button @click="fetchUsers">Fetch Users</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
new Vue({
el: '#app',
data: {
users: []
},
methods: {
fetchUsers() {
// 模拟从服务器获取数据
this.users = [{ name: 'Alice' }, { name: 'Bob' }];
}
}
});
</script>
MC架构 vs MVVM架构
特性 | MC架构 | MVVM架构 |
---|---|---|
组成 | Model + Controller | Model + View + ViewModel |
视图更新 | Controller直接操作DOM | 通过数据绑定自动更新 |
耦合度 | 较高(Controller与DOM紧耦合) | 较低(View与Model完全解耦) |
适用场景 | 小型应用或快速原型 | 中大型复杂应用 |
开发效率 | 简单直接,适合快速开发 | 需要学习数据绑定,但后期维护方便 |
框架支持 | 无特定框架支持 | Vue.js、Knockout.js等框架支持 |
如何选择?
MC架构:
- 适合小型项目或对性能要求较高的场景。
- 不需要复杂的数据绑定机制。
- 开发速度快,但后期维护成本较高。
MVVM架构:
- 适合中大型项目,尤其是需要频繁更新UI的场景。
- 通过数据绑定减少手动操作DOM的代码。
- 学习曲线较高,但长期维护成本低。
总结
- MC架构简单直接,适合快速开发,但耦合度高,难以维护。
- MVVM架构通过数据绑定实现视图与模型的解耦,适合复杂应用,是现代前端开发的主流选择。
JavaScript模板方法模式
JavaScript 模板方法模式
模板方法模式(Template Method Pattern) 是一种行为型设计模式,它定义了一个算法的骨架,并将某些步骤延迟到子类中实现。模板方法模式使得子类可以在不改变算法结构的情况下重新定义算法的某些特定步骤。
模板方法模式的核心思想
算法骨架:
- 在父类中定义算法的骨架,包括一些固定步骤和可变的步骤。
延迟实现:
- 将可变步骤的实现延迟到子类中,子类可以重写这些步骤。
代码复用:
- 通过将通用逻辑放在父类中,避免代码重复。
模板方法模式的组成部分
AbstractClass(抽象类):
- 定义算法的骨架,包括模板方法和抽象方法。
ConcreteClass(具体类):
- 实现抽象类中的抽象方法,完成算法的具体步骤。
模板方法模式的实现
以下是一个简单的示例,展示如何使用模板方法模式实现咖啡和茶的制作过程。
- 定义抽象类
// 抽象类:饮料
class Beverage {
// 模板方法:定义算法的骨架
prepare() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
// 具体方法:固定步骤
boilWater() {
console.log("Boiling water.");
}
// 具体方法:固定步骤
pourInCup() {
console.log("Pouring into cup.");
}
// 抽象方法:可变步骤,由子类实现
brew() {
throw new Error("This method must be overridden.");
}
// 抽象方法:可变步骤,由子类实现
addCondiments() {
throw new Error("This method must be overridden.");
}
}
- 定义具体类
// 具体类:咖啡
class Coffee extends Beverage {
brew() {
console.log("Brewing coffee grounds.");
}
addCondiments() {
console.log("Adding sugar and milk.");
}
}
// 具体类:茶
class Tea extends Beverage {
brew() {
console.log("Steeping the tea.");
}
addCondiments() {
console.log("Adding lemon.");
}
}
- 使用模板方法
// 客户端代码
function prepareBeverage(beverage) {
console.log(`Preparing ${beverage.constructor.name}...`);
beverage.prepare();
console.log("Beverage is ready!\n");
}
// 制作咖啡
const coffee = new Coffee();
prepareBeverage(coffee);
// 制作茶
const tea = new Tea();
prepareBeverage(tea);
输出:
Preparing Coffee...
Boiling water.
Brewing coffee grounds.
Pouring into cup.
Adding sugar and milk.
Beverage is ready!
Preparing Tea...
Boiling water.
Steeping the tea.
Pouring into cup.
Adding lemon.
Beverage is ready!
模板方法模式的优点
代码复用:
- 将通用逻辑放在父类中,避免代码重复。
扩展性:
- 子类可以通过重写可变步骤来扩展算法的行为。
控制结构:
- 父类控制算法的整体结构,子类只关注具体实现。
模板方法模式的缺点
灵活性受限:
- 模板方法模式定义了算法的骨架,子类只能改变某些步骤,无法改变整体结构。
复杂性增加:
- 如果算法的步骤过多,可能会导致类的层次结构变得复杂。
模板方法模式的应用场景
框架设计:
- 在框架中定义通用的算法流程,允许用户自定义某些步骤。
工作流程:
- 在业务流程中定义固定的步骤,允许不同实现类自定义具体操作。
代码复用:
- 当多个类有相似的算法结构时,可以使用模板方法模式提取通用逻辑。
总结
模板方法模式是一种简单但强大的设计模式,适用于定义算法的骨架并允许子类自定义某些步骤的场景。它通过将通用逻辑放在父类中,提高了代码的复用性和可维护性。然而,模板方法模式也可能限制算法的灵活性,因此在设计时需要根据具体需求进行权衡。