前端设计模式
1.工厂模式
前端的工厂模式是一种创建对象的设计模式,旨在封装对象的创建过程,使代码更具灵活性和可维护性。通过工厂模式,开发者无需直接调用构造函数,而是通过工厂函数或类来创建对象。
工厂模式的核心思想
- 封装创建逻辑:将对象的创建逻辑集中在一个地方,便于管理和修改。
- 解耦:调用方无需知道具体的类名或构造细节,只需与工厂交互。
- 扩展性:新增产品类型时,只需修改工厂逻辑,无需改动调用方代码。
适用场景
- 对象的创建逻辑复杂,依赖较多。
- 需要根据条件创建不同类型的对象。
- 希望隐藏对象的创建细节,降低耦合。
示例
假设有一个按钮创建需求,按钮类型包括“主要”和“次要”两种。
// 按钮类
class PrimaryButton {
render() {
return '<button class="primary">Primary Button</button>';
}
}
class SecondaryButton {
render() {
return '<button class="secondary">Secondary Button</button>';
}
}
// 工厂函数
function createButton(type) {
if (type === 'primary') {
return new PrimaryButton();
} else if (type === 'secondary') {
return new SecondaryButton();
}
throw new Error('Unknown button type');
}
// 使用工厂创建按钮
const primaryButton = createButton('primary');
console.log(primaryButton.render()); // 输出: <button class="primary">Primary Button</button>
const secondaryButton = createButton('secondary');
console.log(secondaryButton.render()); // 输出: <button class="secondary">Secondary Button</button>
总结
工厂模式通过封装对象的创建逻辑,提升了代码的可维护性和扩展性,适合复杂对象的创建场景。
2.单例模式
前端的单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式在前端开发中常用于管理全局状态、共享资源或避免重复创建开销较大的对象。
单例模式的核心思想
- 唯一实例:确保一个类只能创建一个实例。
- 全局访问:提供一个全局访问点,方便其他代码获取该实例。
- 延迟初始化:实例只有在第一次被请求时才会创建。
适用场景
- 需要全局共享的资源,如全局状态管理(如 Redux Store)。
- 避免重复创建开销较大的对象,如数据库连接、HTTP 客户端。
- 需要严格控制实例数量的场景,如弹窗组件、日志记录器。
实现方式
以下是 JavaScript 中实现单例模式的几种常见方式:
1. 使用闭包
const Singleton = (function () {
let instance; // 闭包中保存唯一实例
function createInstance() {
const object = new Object("I am the instance");
return object;
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance(); // 延迟初始化
}
return instance;
},
};
})();
// 使用单例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // 输出: true,说明是同一个实例
2. 使用 ES6 类
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
// 使用单例
const instance1 = new Singleton();
const instance2 = new Singleton();
const instance3 = Singleton.getInstance();
console.log(instance1 === instance2); // 输出: true
console.log(instance1 === instance3); // 输出: true
3.策略模式
前端的策略模式(Strategy Pattern)是一种行为设计模式,它允许你定义一系列算法或行为,并将它们封装成独立的类或对象,使得它们可以互相替换。策略模式的核心思想是将算法的使用与算法的实现分离,从而让程序更灵活、可扩展。
策略模式的核心思想
- 定义策略:
- 将不同的算法或行为封装成独立的策略类或对象。
- 上下文(Context):
- 持有一个策略对象的引用,并将具体的任务委托给策略对象。
- 动态切换:
- 在运行时可以根据需要动态切换策略,而不需要修改上下文代码。
策略模式的优点
- 避免条件判断:
- 通过策略模式可以避免大量的
if-else或switch-case条件判断。
- 通过策略模式可以避免大量的
- 易于扩展:
- 新增策略时,只需添加新的策略类,无需修改现有代码。
- 代码复用:
- 策略类可以在不同的上下文中复用。
- 清晰的结构:
- 将算法的实现与使用分离,代码结构更清晰。
策略模式的实现
在前端中,策略模式可以通过对象、类或函数来实现。以下是几种常见的实现方式。
1. 使用对象实现策略模式
将不同的策略封装成对象,通过键值对的方式动态选择策略。
// 定义策略对象
const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
// 上下文函数
function calculate(strategy, a, b) {
return strategies[strategy](a, b);
}
// 使用策略
console.log(calculate('add', 10, 5)); // 输出: 15
console.log(calculate('subtract', 10, 5)); // 输出: 5
console.log(calculate('multiply', 10, 5)); // 输出: 50
console.log(calculate('divide', 10, 5)); // 输出: 2
2. 使用类实现策略模式
将不同的策略封装成类,通过上下文类动态选择策略。
// 定义策略类
class AddStrategy {
execute(a, b) {
return a + b;
}
}
class SubtractStrategy {
execute(a, b) {
return a - b;
}
}
class MultiplyStrategy {
execute(a, b) {
return a * b;
}
}
class DivideStrategy {
execute(a, b) {
return a / b;
}
}
// 上下文类
class Calculator {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
calculate(a, b) {
return this.strategy.execute(a, b);
}
}
// 使用策略
const calculator = new Calculator(new AddStrategy());
console.log(calculator.calculate(10, 5)); // 输出: 15
calculator.setStrategy(new SubtractStrategy());
console.log(calculator.calculate(10, 5)); // 输出: 5
3. 使用函数实现策略模式
将不同的策略封装成函数,通过上下文函数动态选择策略。
javascript
复制
// 定义策略函数
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
return a / b;
}
// 上下文函数
function calculate(strategy, a, b) {
return strategy(a, b);
}
// 使用策略
console.log(calculate(add, 10, 5)); // 输出: 15
console.log(calculate(subtract, 10, 5)); // 输出: 5
console.log(calculate(multiply, 10, 5)); // 输出: 50
console.log(calculate(divide, 10, 5)); // 输出: 2
实际应用场景
策略模式在前端开发中有广泛的应用,以下是一些常见场景:
1. 表单验证
不同的表单字段可能需要不同的验证规则,可以使用策略模式将验证规则封装成策略。
javascript
复制
const validationStrategies = {
isNonEmpty: (value) => (value === '' ? '字段不能为空' : ''),
isNumber: (value) => (isNaN(value) ? '字段必须为数字' : ''),
isEmail: (value) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? '' : '邮箱格式不正确'),
};
function validate(formData, rules) {
return Object.keys(rules).reduce((errors, field) => {
const rule = rules[field];
const value = formData[field];
const error = validationStrategies[rule](value);
if (error) {
errors[field] = error;
}
return errors;
}, {});
}
// 使用策略
const formData = { name: '', age: 'abc', email: 'test' };
const rules = { name: 'isNonEmpty', age: 'isNumber', email: 'isEmail' };
console.log(validate(formData, rules));
// 输出: { name: '字段不能为空', age: '字段必须为数字', email: '邮箱格式不正确' }
2. 动态切换 UI 主题
可以根据用户的选择动态切换 UI 主题,将不同的主题封装成策略。
javascript
复制
const themes = {
light: { background: '#fff', color: '#000' },
dark: { background: '#333', color: '#fff' },
blue: { background: '#007bff', color: '#fff' },
};
function applyTheme(theme) {
const { background, color } = themes[theme];
document.body.style.background = background;
document.body.style.color = color;
}
// 使用策略
applyTheme('light'); // 应用浅色主题
applyTheme('dark'); // 应用深色主题
总结
- 策略模式通过将算法或行为封装成独立的策略,使得它们可以互相替换。
- 在前端中,策略模式可以避免大量的条件判断,提高代码的可读性和可维护性。
- 常见的实现方式包括对象、类和函数。
- 实际应用场景包括表单验证、动态切换 UI 主题等。
4.发布订阅模式
前端的发布-订阅模式(Publish-Subscribe Pattern,简称 Pub/Sub)是一种行为设计模式,用于在对象之间定义一种一对多的依赖关系。当一个对象(发布者)的状态发生变化时,所有依赖它的对象(订阅者)都会收到通知并自动更新。
发布-订阅模式的核心思想
- 发布者(Publisher):
- 负责发布消息或事件。
- 不关心谁订阅了消息,也不关心订阅者如何处理消息。
- 订阅者(Subscriber):
- 订阅感兴趣的消息或事件。
- 在消息发布时执行相应的处理逻辑。
- 事件中心(Event Channel):
- 作为发布者和订阅者之间的中介,负责管理订阅关系和消息分发。
发布-订阅模式的优点
- 解耦:
- 发布者和订阅者之间没有直接依赖,通过事件中心通信,降低了耦合度。
- 灵活性:
- 可以动态添加或移除订阅者,系统扩展性更强。
- 可维护性:
- 代码结构清晰,易于维护和调试。
发布-订阅模式的实现
在前端中,发布-订阅模式可以通过自定义事件中心或使用现成的库(如 EventEmitter)来实现。
1. 自定义事件中心
以下是一个简单的事件中心实现:
class EventBus {
constructor() {
this.events = {}; // 存储事件及其对应的回调函数列表
}
// 订阅事件
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []; // 初始化事件回调列表
}
this.events[event].push(callback); // 添加回调函数
}
// 发布事件
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach((callback) => {
callback(...args); // 执行回调函数
});
}
}
// 取消订阅
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter((cb) => cb !== callback); // 移除回调函数
}
}
}
// 使用事件中心
const eventBus = new EventBus();
// 订阅事件
eventBus.on('message', (data) => {
console.log('收到消息:', data);
});
// 发布事件
eventBus.emit('message', 'Hello, World!'); // 输出: 收到消息: Hello, World!
5.适配器模式
前端的适配器模式(Adapter Pattern)是一种结构型设计模式,用于将一个类的接口转换成客户端所期望的另一种接口。适配器模式的核心思想是解决接口不兼容的问题,使得原本不兼容的类可以一起工作。
适配器模式的核心思想
- 目标接口(Target):
- 客户端期望的接口。
- 适配者(Adaptee):
- 需要被适配的类或对象,其接口与目标接口不兼容。
- 适配器(Adapter):
- 将适配者的接口转换成目标接口,使客户端可以调用适配者的功能。
适配器模式的优点
- 兼容性:
- 可以让不兼容的接口一起工作,提高代码的复用性。
- 解耦:
- 客户端与适配者之间没有直接依赖,通过适配器进行通信。
- 灵活性:
- 可以动态适配不同的接口,扩展性更强。
适配器模式的实现
在前端中,适配器模式可以通过类、对象或函数来实现。以下是几种常见的实现方式。
1. 类适配器
通过继承适配者类并实现目标接口来实现适配器。
// 目标接口
class Target {
request() {
return 'Target: 默认行为';
}
}
// 适配者类
class Adaptee {
specificRequest() {
return 'Adaptee: 特殊行为';
}
}
// 适配器类
class Adapter extends Adaptee {
request() {
return `Adapter: 转换后的 ${this.specificRequest()}`;
}
}
// 使用适配器
const target = new Target();
console.log(target.request()); // 输出: Target: 默认行为
const adapter = new Adapter();
console.log(adapter.request()); // 输出: Adapter: 转换后的 Adaptee: 特殊行为
2. 对象适配器
通过组合适配者对象并实现目标接口来实现适配器。
// 目标接口
class Target {
request() {
return 'Target: 默认行为';
}
}
// 适配者类
class Adaptee {
specificRequest() {
return 'Adaptee: 特殊行为';
}
}
// 适配器类
class Adapter {
constructor(adaptee) {
this.adaptee = adaptee;
}
request() {
return `Adapter: 转换后的 ${this.adaptee.specificRequest()}`;
}
}
// 使用适配器
const target = new Target();
console.log(target.request()); // 输出: Target: 默认行为
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // 输出: Adapter: 转换后的 Adaptee: 特殊行为
3. 函数适配器
通过函数包装来实现适配器。
// 目标函数
function targetRequest() {
return 'Target: 默认行为';
}
// 适配者函数
function adapteeSpecificRequest() {
return 'Adaptee: 特殊行为';
}
// 适配器函数
function adapterRequest() {
return `Adapter: 转换后的 ${adapteeSpecificRequest()}`;
}
// 使用适配器
console.log(targetRequest()); // 输出: Target: 默认行为
console.log(adapterRequest()); // 输出: Adapter: 转换后的 Adaptee: 特殊行为
实际应用场景
适配器模式在前端开发中有广泛的应用,以下是一些常见场景:
1. 兼容不同 API
当需要调用不同第三方库的 API 时,可以使用适配器模式统一接口。
// 第三方库 A
class LibraryA {
specificRequest() {
return 'LibraryA: 特殊行为';
}
}
// 第三方库 B
class LibraryB {
differentRequest() {
return 'LibraryB: 不同行为';
}
}
// 适配器
class Adapter {
constructor(library) {
this.library = library;
}
request() {
if (this.library instanceof LibraryA) {
return `Adapter: 转换后的 ${this.library.specificRequest()}`;
} else if (this.library instanceof LibraryB) {
return `Adapter: 转换后的 ${this.library.differentRequest()}`;
}
return 'Adapter: 未知库';
}
}
// 使用适配器
const libraryA = new LibraryA();
const adapterA = new Adapter(libraryA);
console.log(adapterA.request()); // 输出: Adapter: 转换后的 LibraryA: 特殊行为
const libraryB = new LibraryB();
const adapterB = new Adapter(libraryB);
console.log(adapterB.request()); // 输出: Adapter: 转换后的 LibraryB: 不同行为
2. 数据格式转换
当需要将一种数据格式转换成另一种格式时,可以使用适配器模式。
// 旧数据格式
const oldData = {
name: 'John',
age: 30,
};
// 适配器函数
function dataAdapter(oldData) {
return {
fullName: oldData.name,
userAge: oldData.age,
};
}
// 使用适配器
const newData = dataAdapter(oldData);
console.log(newData); // 输出: { fullName: 'John', userAge: 30 }
3. 兼容不同浏览器
当需要兼容不同浏览器的 API 时,可以使用适配器模式。
// 浏览器 A 的 API
function browserARequest() {
return 'BrowserA: 特殊行为';
}
// 浏览器 B 的 API
function browserBRequest() {
return 'BrowserB: 不同行为';
}
// 适配器函数
function adapterRequest() {
if (typeof browserARequest === 'function') {
return `Adapter: 转换后的 ${browserARequest()}`;
} else if (typeof browserBRequest === 'function') {
return `Adapter: 转换后的 ${browserBRequest()}`;
}
return 'Adapter: 未知浏览器';
}
// 使用适配器
console.log(adapterRequest()); // 根据浏览器输出不同的结果
总结
- 适配器模式用于解决接口不兼容的问题,将一个接口转换成另一个接口。
- 在前端中,适配器模式常用于兼容不同 API、数据格式转换和浏览器兼容性处理。
- 可以通过类、对象或函数来实现适配器模式。
- 适配器模式提高了代码的复用性和灵活性,是前端开发中常用的设计模式之一。
6.责任链模式
前端的责任链模式(Chain of Responsibility Pattern)是一种行为设计模式,它允许多个对象有机会处理请求,从而避免请求的发送者与接收者之间的耦合。责任链模式将这些对象连成一条链,并沿着这条链传递请求,直到有对象处理它为止。
责任链模式的核心思想
- 处理者(Handler):
- 定义一个处理请求的接口,并持有下一个处理者的引用。
- 具体处理者(Concrete Handler):
- 实现处理请求的具体逻辑,如果自己不能处理,则将请求传递给下一个处理者。
- 客户端(Client):
- 创建责任链,并向链的第一个处理者发送请求。
责任链模式的优点
- 解耦:
- 请求的发送者和处理者之间没有直接依赖,降低了耦合度。
- 灵活性:
- 可以动态地调整责任链中的处理者顺序或增减处理者。
- 可扩展性:
- 新增处理者时无需修改现有代码,符合开闭原则。
责任链模式的实现
在前端中,责任链模式可以通过类或函数来实现。以下是几种常见的实现方式。
1. 使用类实现责任链模式
通过类定义处理者和具体处理者,并使用 next 属性指向下一个处理者。
// 处理者基类
class Handler {
constructor() {
this.next = null; // 下一个处理者
}
setNext(handler) {
this.next = handler;
return handler; // 返回下一个处理者,方便链式调用
}
handle(request) {
if (this.next) {
return this.next.handle(request); // 传递给下一个处理者
}
return null; // 链的末尾,没有处理者能处理请求
}
}
// 具体处理者 A
class ConcreteHandlerA extends Handler {
handle(request) {
if (request === 'A') {
return `ConcreteHandlerA 处理了请求: ${request}`;
}
return super.handle(request); // 传递给下一个处理者
}
}
// 具体处理者 B
class ConcreteHandlerB extends Handler {
handle(request) {
if (request === 'B') {
return `ConcreteHandlerB 处理了请求: ${request}`;
}
return super.handle(request); // 传递给下一个处理者
}
}
// 具体处理者 C
class ConcreteHandlerC extends Handler {
handle(request) {
if (request === 'C') {
return `ConcreteHandlerC 处理了请求: ${request}`;
}
return super.handle(request); // 传递给下一个处理者
}
}
// 使用责任链
const handlerA = new ConcreteHandlerA();
const handlerB = new ConcreteHandlerB();
const handlerC = new ConcreteHandlerC();
handlerA.setNext(handlerB).setNext(handlerC); // 构建责任链
console.log(handlerA.handle('A')); // 输出: ConcreteHandlerA 处理了请求: A
console.log(handlerA.handle('B')); // 输出: ConcreteHandlerB 处理了请求: B
console.log(handlerA.handle('C')); // 输出: ConcreteHandlerC 处理了请求: C
console.log(handlerA.handle('D')); // 输出: null(没有处理者能处理请求)
2. 使用函数实现责任链模式
通过函数定义处理逻辑,并使用 next 参数指向下一个处理者。
// 处理者函数
function handlerA(request, next) {
if (request === 'A') {
return `handlerA 处理了请求: ${request}`;
}
return next(request); // 传递给下一个处理者
}
function handlerB(request, next) {
if (request === 'B') {
return `handlerB 处理了请求: ${request}`;
}
return next(request); // 传递给下一个处理者
}
function handlerC(request, next) {
if (request === 'C') {
return `handlerC 处理了请求: ${request}`;
}
return next(request); // 传递给下一个处理者
}
// 构建责任链
function createChain(...handlers) {
return function (request) {
let index = 0;
function next(request) {
if (index < handlers.length) {
return handlers[index++](request, next);
}
return null; // 链的末尾,没有处理者能处理请求
}
return next(request);
};
}
// 使用责任链
const chain = createChain(handlerA, handlerB, handlerC);
console.log(chain('A')); // 输出: handlerA 处理了请求: A
console.log(chain('B')); // 输出: handlerB 处理了请求: B
console.log(chain('C')); // 输出: handlerC 处理了请求: C
console.log(chain('D')); // 输出: null(没有处理者能处理请求)
实际应用场景
责任链模式在前端开发中有广泛的应用,以下是一些常见场景:
1. 表单验证
将表单验证规则封装成责任链,依次验证每个字段。
// 验证规则
function validateNotEmpty(value, next) {
if (value === '') {
return '字段不能为空';
}
return next(value);
}
function validateEmail(value, next) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return '邮箱格式不正确';
}
return next(value);
}
function validateLength(value, next) {
if (value.length < 6) {
return '长度不能少于6个字符';
}
return next(value);
}
// 构建责任链
const validateChain = createChain(validateNotEmpty, validateEmail, validateLength);
// 使用责任链
console.log(validateChain('')); // 输出: 字段不能为空
console.log(validateChain('test')); // 输出: 邮箱格式不正确
console.log(validateChain('test@example.com')); // 输出: 长度不能少于6个字符
console.log(validateChain('valid@example.com')); // 输出: null(验证通过)
2. 中间件机制
在 Express 或 Koa 等框架中,中间件机制就是责任链模式的典型应用。
// 模拟 Koa 中间件机制
function createMiddlewareChain(...middlewares) {
return function (context, next) {
let index = 0;
function dispatch(i) {
if (i < middlewares.length) {
return middlewares[i](context, dispatch.bind(null, i + 1));
}
return next(context); // 最后一个中间件
}
return dispatch(0);
};
}
// 使用中间件
const middlewareChain = createMiddlewareChain(
(ctx, next) => {
console.log('Middleware 1');
next();
},
(ctx, next) => {
console.log('Middleware 2');
next();
},
(ctx, next) => {
console.log('Middleware 3');
next();
}
);
middlewareChain({}, () => {
console.log('End of chain');
});
// 输出:
// Middleware 1
// Middleware 2
// Middleware 3
// End of chain
总结
- 责任链模式通过将多个处理者连成一条链,依次处理请求,直到有处理者能够处理为止。
- 在前端中,责任链模式常用于表单验证、中间件机制等场景。
- 可以通过类或函数来实现责任链模式。
- 责任链模式提高了代码的灵活性和可扩展性,是前端开发中常用的设计模式之一。
7.命令模式
前端的命令模式(Command Pattern)是一种行为设计模式,它将请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化,并支持请求的排队、记录、撤销等操作。命令模式的核心思想是将“请求”与“执行”解耦,使得请求的发送者和接收者之间没有直接依赖。
命令模式的核心思想
- 命令(Command):
- 封装了请求的对象,定义了执行请求的接口。
- 接收者(Receiver):
- 实际执行请求的对象,知道如何完成具体的操作。
- 调用者(Invoker):
- 持有命令对象,并触发命令的执行。
- 客户端(Client):
- 创建命令对象并设置其接收者。
命令模式的优点
- 解耦:
- 请求的发送者和接收者之间没有直接依赖,通过命令对象进行通信。
- 扩展性:
- 可以方便地添加新的命令,而无需修改现有代码。
- 支持高级操作:
- 可以轻松实现请求的排队、记录、撤销等功能。
命令模式的实现
在前端中,命令模式可以通过类或函数来实现。以下是几种常见的实现方式。
1. 使用类实现命令模式
通过类定义命令、接收者和调用者。
// 接收者
class Receiver {
action() {
console.log('接收者执行操作');
}
}
// 命令接口
class Command {
execute() {
throw new Error('子类必须实现 execute 方法');
}
}
// 具体命令
class ConcreteCommand extends Command {
constructor(receiver) {
super();
this.receiver = receiver;
}
execute() {
this.receiver.action();
}
}
// 调用者
class Invoker {
setCommand(command) {
this.command = command;
}
executeCommand() {
this.command.execute();
}
}
// 使用命令模式
const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
const invoker = new Invoker();
invoker.setCommand(command);
invoker.executeCommand(); // 输出: 接收者执行操作
2. 使用函数实现命令模式
通过函数定义命令和接收者。
// 接收者
const receiver = {
action: () => {
console.log('接收者执行操作');
},
};
// 命令函数
function createCommand(receiver) {
return function () {
receiver.action();
};
}
// 调用者
function invoker(command) {
command();
}
// 使用命令模式
const command = createCommand(receiver);
invoker(command); // 输出: 接收者执行操作
实际应用场景
命令模式在前端开发中有广泛的应用,以下是一些常见场景:
1. 撤销和重做
命令模式可以轻松实现撤销和重做功能。
// 接收者
class Editor {
constructor() {
this.content = '';
}
write(text) {
this.content += text;
}
undo() {
this.content = this.content.slice(0, -1);
}
getContent() {
return this.content;
}
}
// 命令接口
class Command {
execute() {
throw new Error('子类必须实现 execute 方法');
}
undo() {
throw new Error('子类必须实现 undo 方法');
}
}
// 具体命令
class WriteCommand extends Command {
constructor(editor, text) {
super();
this.editor = editor;
this.text = text;
}
execute() {
this.editor.write(this.text);
}
undo() {
this.editor.undo();
}
}
// 调用者
class Invoker {
constructor() {
this.commands = [];
this.history = [];
}
executeCommand(command) {
command.execute();
this.commands.push(command);
}
undo() {
const command = this.commands.pop();
if (command) {
command.undo();
this.history.push(command);
}
}
redo() {
const command = this.history.pop();
if (command) {
command.execute();
this.commands.push(command);
}
}
}
// 使用命令模式
const editor = new Editor();
const invoker = new Invoker();
invoker.executeCommand(new WriteCommand(editor, 'Hello, '));
invoker.executeCommand(new WriteCommand(editor, 'World!'));
console.log(editor.getContent()); // 输出: Hello, World!
invoker.undo();
console.log(editor.getContent()); // 输出: Hello, World
invoker.redo();
console.log(editor.getContent()); // 输出: Hello, World!
2. 菜单和按钮操作
在 UI 框架中,命令模式可以用于实现菜单项或按钮的操作。
// 接收者
class Menu {
open() {
console.log('打开菜单');
}
close() {
console.log('关闭菜单');
}
}
// 命令接口
class Command {
execute() {
throw new Error('子类必须实现 execute 方法');
}
}
// 具体命令
class OpenMenuCommand extends Command {
constructor(menu) {
super();
this.menu = menu;
}
execute() {
this.menu.open();
}
}
class CloseMenuCommand extends Command {
constructor(menu) {
super();
this.menu = menu;
}
execute() {
this.menu.close();
}
}
// 调用者
class Button {
constructor(command) {
this.command = command;
}
onClick() {
this.command.execute();
}
}
// 使用命令模式
const menu = new Menu();
const openButton = new Button(new OpenMenuCommand(menu));
const closeButton = new Button(new CloseMenuCommand(menu));
openButton.onClick(); // 输出: 打开菜单
closeButton.onClick(); // 输出: 关闭菜单
3. 异步任务队列
命令模式可以用于实现异步任务的队列化执行。
// 接收者
class TaskQueue {
constructor() {
this.tasks = [];
}
addTask(task) {
this.tasks.push(task);
}
executeTasks() {
this.tasks.forEach((task) => task());
this.tasks = [];
}
}
// 命令函数
function createTask(task) {
return function () {
console.log(`执行任务: ${task}`);
};
}
// 使用命令模式
const taskQueue = new TaskQueue();
taskQueue.addTask(createTask('任务1'));
taskQueue.addTask(createTask('任务2'));
taskQueue.addTask(createTask('任务3'));
taskQueue.executeTasks();
// 输出:
// 执行任务: 任务1
// 执行任务: 任务2
// 执行任务: 任务3
总结
- 命令模式通过将请求封装成对象,解耦了请求的发送者和接收者。
- 在前端中,命令模式常用于实现撤销/重做、菜单操作、异步任务队列等功能。
- 可以通过类或函数来实现命令模式。
- 命令模式提高了代码的灵活性和可扩展性,是前端开发中常用的设计模式之一。
8.状态模式
前端的**状态模式(State Pattern)**是一种行为设计模式,它允许一个对象在其内部状态改变时改变其行为。状态模式的核心思想是将对象的行为封装到不同的状态类中,使得对象在不同状态下表现出不同的行为,同时避免了大量的条件判断语句。
状态模式的核心思想
- 上下文(Context):
- 持有当前状态的引用,并将请求委托给当前状态对象处理。
- 状态接口(State Interface):
- 定义所有具体状态类需要实现的方法。
- 具体状态(Concrete State):
- 实现状态接口,定义在特定状态下的行为。
状态模式的优点
- 消除条件判断:
- 通过将行为分散到不同的状态类中,避免了大量的
if-else或switch-case语句。
- 通过将行为分散到不同的状态类中,避免了大量的
- 易于扩展:
- 新增状态时只需添加新的状态类,无需修改现有代码。
- 提高可维护性:
- 每个状态类的职责单一,代码结构更清晰。
状态模式的实现
在前端中,状态模式可以通过类或对象来实现。以下是几种常见的实现方式。
1. 使用类实现状态模式
通过类定义上下文和具体状态。
// 状态接口
class State {
handle(context) {
throw new Error('子类必须实现 handle 方法');
}
}
// 具体状态 A
class ConcreteStateA extends State {
handle(context) {
console.log('当前状态是 A');
context.setState(new ConcreteStateB()); // 切换到状态 B
}
}
// 具体状态 B
class ConcreteStateB extends State {
handle(context) {
console.log('当前状态是 B');
context.setState(new ConcreteStateA()); // 切换到状态 A
}
}
// 上下文
class Context {
constructor(state) {
this.state = state;
}
setState(state) {
this.state = state;
}
request() {
this.state.handle(this);
}
}
// 使用状态模式
const context = new Context(new ConcreteStateA());
context.request(); // 输出: 当前状态是 A
context.request(); // 输出: 当前状态是 B
context.request(); // 输出: 当前状态是 A
2. 使用对象实现状态模式
通过对象定义上下文和具体状态。
// 状态对象
const states = {
A: {
handle(context) {
console.log('当前状态是 A');
context.setState(states.B); // 切换到状态 B
},
},
B: {
handle(context) {
console.log('当前状态是 B');
context.setState(states.A); // 切换到状态 A
},
},
};
// 上下文
const context = {
state: states.A,
setState(state) {
this.state = state;
},
request() {
this.state.handle(this);
},
};
// 使用状态模式
context.request(); // 输出: 当前状态是 A
context.request(); // 输出: 当前状态是 B
context.request(); // 输出: 当前状态是 A
9.享元模式
前端的**享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享尽可能多的相似对象来减少内存使用和提高性能。享元模式的核心思想是将对象的内部状态(Intrinsic State)和外部状态(Extrinsic State)**分离,内部状态可以被共享,而外部状态由客户端传递。
享元模式的核心思想
-
内部状态(Intrinsic State):
- 对象的固有属性,可以被多个对象共享。
- 例如:文本编辑器中的字符样式(字体、颜色等)。
-
外部状态(Extrinsic State):
- 对象的可变属性,由客户端传递。
- 例如:文本编辑器中的字符位置。
-
享元工厂(Flyweight Factory):
- 负责创建和管理享元对象,确保共享的内部状态被复用。
享元模式的优点
- 减少内存占用:
- 通过共享内部状态,减少重复对象的创建。
- 提高性能:
- 减少了对象的创建和销毁开销。
- 简化对象管理:
- 享元工厂集中管理共享对象,便于维护。
享元模式的实现
在前端中,享元模式可以通过对象池、缓存或共享数据来实现。以下是几种常见的实现方式。
1. 使用对象池实现享元模式
通过对象池管理共享对象,避免重复创建。
// 享元对象
class Character {
constructor(char) {
this.char = char; // 内部状态
}
render(font, size) {
console.log(`渲染字符 ${this.char},字体: ${font},大小: ${size}`);
}
}
// 享元工厂
class CharacterFactory {
constructor() {
this.characters = {}; // 对象池
}
getCharacter(char) {
if (!this.characters[char]) {
this.characters[char] = new Character(char); // 创建新对象
}
return this.characters[char]; // 返回共享对象
}
}
// 使用享元模式
const factory = new CharacterFactory();
const charA = factory.getCharacter('A');
charA.render('Arial', 12); // 输出: 渲染字符 A,字体: Arial,大小: 12
const charB = factory.getCharacter('B');
charB.render('Times New Roman', 14); // 输出: 渲染字符 B,字体: Times New Roman,大小: 14
const charA2 = factory.getCharacter('A');
charA2.render('Verdana', 16); // 输出: 渲染字符 A,字体: Verdana,大小: 16
console.log(charA === charA2); // 输出: true(共享同一个对象)
2. 使用缓存实现享元模式
通过缓存管理共享对象,避免重复创建。
// 享元对象
class Image {
constructor(url) {
this.url = url; // 内部状态
}
render(x, y) {
console.log(`渲染图片 ${this.url},位置: (${x}, ${y})`);
}
}
// 享元工厂
class ImageFactory {
constructor() {
this.images = {}; // 缓存
}
getImage(url) {
if (!this.images[url]) {
this.images[url] = new Image(url); // 创建新对象
}
return this.images[url]; // 返回共享对象
}
}
// 使用享元模式
const factory = new ImageFactory();
const image1 = factory.getImage('image1.png');
image1.render(10, 20); // 输出: 渲染图片 image1.png,位置: (10, 20)
const image2 = factory.getImage('image2.png');
image2.render(30, 40); // 输出: 渲染图片 image2.png,位置: (30, 40)
const image1Copy = factory.getImage('image1.png');
image1Copy.render(50, 60); // 输出: 渲染图片 image1.png,位置: (50, 60)
console.log(image1 === image1Copy); // 输出: true(共享同一个对象)
实际应用场景
享元模式在前端开发中有广泛的应用,以下是一些常见场景:
1. 文本编辑器
在文本编辑器中,字符的样式(如字体、颜色)可以作为内部状态共享,而字符的位置可以作为外部状态传递。
// 享元对象
class CharacterStyle {
constructor(font, color) {
this.font = font;
this.color = color;
}
render(char, x, y) {
console.log(`渲染字符 ${char},字体: ${this.font},颜色: ${this.color},位置: (${x}, ${y})`);
}
}
// 享元工厂
class CharacterStyleFactory {
constructor() {
this.styles = {}; // 缓存
}
getStyle(font, color) {
const key = `${font}-${color}`;
if (!this.styles[key]) {
this.styles[key] = new CharacterStyle(font, color); // 创建新对象
}
return this.styles[key]; // 返回共享对象
}
}
// 使用享元模式
const factory = new CharacterStyleFactory();
const style1 = factory.getStyle('Arial', 'red');
style1.render('A', 10, 20); // 输出: 渲染字符 A,字体: Arial,颜色: red,位置: (10, 20)
const style2 = factory.getStyle('Times New Roman', 'blue');
style2.render('B', 30, 40); // 输出: 渲染字符 B,字体: Times New Roman,颜色: blue,位置: (30, 40)
const style1Copy = factory.getStyle('Arial', 'red');
style1Copy.render('C', 50, 60); // 输出: 渲染字符 C,字体: Arial,颜色: red,位置: (50, 60)
console.log(style1 === style1Copy); // 输出: true(共享同一个对象)
2. 游戏开发
在游戏开发中,角色的外观(如皮肤、装备)可以作为内部状态共享,而角色的位置和动作可以作为外部状态传递。
// 享元对象
class Skin {
constructor(texture) {
this.texture = texture;
}
render(x, y) {
console.log(`渲染皮肤 ${this.texture},位置: (${x}, ${y})`);
}
}
// 享元工厂
class SkinFactory {
constructor() {
this.skins = {}; // 缓存
}
getSkin(texture) {
if (!this.skins[texture]) {
this.skins[texture] = new Skin(texture); // 创建新对象
}
return this.skins[texture]; // 返回共享对象
}
}
// 使用享元模式
const factory = new SkinFactory();
const skin1 = factory.getSkin('skin1.png');
skin1.render(10, 20); // 输出: 渲染皮肤 skin1.png,位置: (10, 20)
const skin2 = factory.getSkin('skin2.png');
skin2.render(30, 40); // 输出: 渲染皮肤 skin2.png,位置: (30, 40)
const skin1Copy = factory.getSkin('skin1.png');
skin1Copy.render(50, 60); // 输出: 渲染皮肤 skin1.png,位置: (50, 60)
console.log(skin1 === skin1Copy); // 输出: true(共享同一个对象)
总结
- 享元模式通过共享内部状态来减少内存占用和提高性能。
- 在前端中,享元模式常用于文本编辑器、游戏开发等需要大量相似对象的场景。
- 可以通过对象池、缓存或共享数据来实现享元模式。
- 享元模式的核心是将内部状态和外部状态分离,确保内部状态可以被共享。
10.状态机
状态机模式(FSM)注重的是「事件驱动状态转移」;而状态模式注重的是「状态驱动行为变化」。