前端设计模式

45 阅读15分钟

前端设计模式

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.单例模式

前端的单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式在前端开发中常用于管理全局状态、共享资源或避免重复创建开销较大的对象。


单例模式的核心思想

  1. 唯一实例:确保一个类只能创建一个实例。
  2. 全局访问:提供一个全局访问点,方便其他代码获取该实例。
  3. 延迟初始化:实例只有在第一次被请求时才会创建。

适用场景

  • 需要全局共享的资源,如全局状态管理(如 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)是一种行为设计模式,它允许你定义一系列算法或行为,并将它们封装成独立的类或对象,使得它们可以互相替换。策略模式的核心思想是将算法的使用与算法的实现分离,从而让程序更灵活、可扩展。


策略模式的核心思想

  1. 定义策略
    • 将不同的算法或行为封装成独立的策略类或对象。
  2. 上下文(Context)
    • 持有一个策略对象的引用,并将具体的任务委托给策略对象。
  3. 动态切换
    • 在运行时可以根据需要动态切换策略,而不需要修改上下文代码。

策略模式的优点

  1. 避免条件判断
    • 通过策略模式可以避免大量的 if-elseswitch-case 条件判断。
  2. 易于扩展
    • 新增策略时,只需添加新的策略类,无需修改现有代码。
  3. 代码复用
    • 策略类可以在不同的上下文中复用。
  4. 清晰的结构
    • 将算法的实现与使用分离,代码结构更清晰。

策略模式的实现

在前端中,策略模式可以通过对象、类或函数来实现。以下是几种常见的实现方式。

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)是一种行为设计模式,用于在对象之间定义一种一对多的依赖关系。当一个对象(发布者)的状态发生变化时,所有依赖它的对象(订阅者)都会收到通知并自动更新。


发布-订阅模式的核心思想

  1. 发布者(Publisher)
    • 负责发布消息或事件。
    • 不关心谁订阅了消息,也不关心订阅者如何处理消息。
  2. 订阅者(Subscriber)
    • 订阅感兴趣的消息或事件。
    • 在消息发布时执行相应的处理逻辑。
  3. 事件中心(Event Channel)
    • 作为发布者和订阅者之间的中介,负责管理订阅关系和消息分发。

发布-订阅模式的优点

  1. 解耦
    • 发布者和订阅者之间没有直接依赖,通过事件中心通信,降低了耦合度。
  2. 灵活性
    • 可以动态添加或移除订阅者,系统扩展性更强。
  3. 可维护性
    • 代码结构清晰,易于维护和调试。

发布-订阅模式的实现

在前端中,发布-订阅模式可以通过自定义事件中心或使用现成的库(如 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)是一种结构型设计模式,用于将一个类的接口转换成客户端所期望的另一种接口。适配器模式的核心思想是解决接口不兼容的问题,使得原本不兼容的类可以一起工作。


适配器模式的核心思想

  1. 目标接口(Target)
    • 客户端期望的接口。
  2. 适配者(Adaptee)
    • 需要被适配的类或对象,其接口与目标接口不兼容。
  3. 适配器(Adapter)
    • 将适配者的接口转换成目标接口,使客户端可以调用适配者的功能。

适配器模式的优点

  1. 兼容性
    • 可以让不兼容的接口一起工作,提高代码的复用性。
  2. 解耦
    • 客户端与适配者之间没有直接依赖,通过适配器进行通信。
  3. 灵活性
    • 可以动态适配不同的接口,扩展性更强。

适配器模式的实现

在前端中,适配器模式可以通过类、对象或函数来实现。以下是几种常见的实现方式。

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)是一种行为设计模式,它允许多个对象有机会处理请求,从而避免请求的发送者与接收者之间的耦合。责任链模式将这些对象连成一条链,并沿着这条链传递请求,直到有对象处理它为止。


责任链模式的核心思想

  1. 处理者(Handler)
    • 定义一个处理请求的接口,并持有下一个处理者的引用。
  2. 具体处理者(Concrete Handler)
    • 实现处理请求的具体逻辑,如果自己不能处理,则将请求传递给下一个处理者。
  3. 客户端(Client)
    • 创建责任链,并向链的第一个处理者发送请求。

责任链模式的优点

  1. 解耦
    • 请求的发送者和处理者之间没有直接依赖,降低了耦合度。
  2. 灵活性
    • 可以动态地调整责任链中的处理者顺序或增减处理者。
  3. 可扩展性
    • 新增处理者时无需修改现有代码,符合开闭原则。

责任链模式的实现

在前端中,责任链模式可以通过类或函数来实现。以下是几种常见的实现方式。

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)是一种行为设计模式,它将请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化,并支持请求的排队、记录、撤销等操作。命令模式的核心思想是将“请求”与“执行”解耦,使得请求的发送者和接收者之间没有直接依赖。


命令模式的核心思想

  1. 命令(Command)
    • 封装了请求的对象,定义了执行请求的接口。
  2. 接收者(Receiver)
    • 实际执行请求的对象,知道如何完成具体的操作。
  3. 调用者(Invoker)
    • 持有命令对象,并触发命令的执行。
  4. 客户端(Client)
    • 创建命令对象并设置其接收者。

命令模式的优点

  1. 解耦
    • 请求的发送者和接收者之间没有直接依赖,通过命令对象进行通信。
  2. 扩展性
    • 可以方便地添加新的命令,而无需修改现有代码。
  3. 支持高级操作
    • 可以轻松实现请求的排队、记录、撤销等功能。

命令模式的实现

在前端中,命令模式可以通过类或函数来实现。以下是几种常见的实现方式。

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)**是一种行为设计模式,它允许一个对象在其内部状态改变时改变其行为。状态模式的核心思想是将对象的行为封装到不同的状态类中,使得对象在不同状态下表现出不同的行为,同时避免了大量的条件判断语句。


状态模式的核心思想

  1. 上下文(Context)
    • 持有当前状态的引用,并将请求委托给当前状态对象处理。
  2. 状态接口(State Interface)
    • 定义所有具体状态类需要实现的方法。
  3. 具体状态(Concrete State)
    • 实现状态接口,定义在特定状态下的行为。

状态模式的优点

  1. 消除条件判断
    • 通过将行为分散到不同的状态类中,避免了大量的 if-elseswitch-case 语句。
  2. 易于扩展
    • 新增状态时只需添加新的状态类,无需修改现有代码。
  3. 提高可维护性
    • 每个状态类的职责单一,代码结构更清晰。

状态模式的实现

在前端中,状态模式可以通过类或对象来实现。以下是几种常见的实现方式。

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)**分离,内部状态可以被共享,而外部状态由客户端传递。


享元模式的核心思想

  1. 内部状态(Intrinsic State)

    • 对象的固有属性,可以被多个对象共享。
    • 例如:文本编辑器中的字符样式(字体、颜色等)。
  2. 外部状态(Extrinsic State)

    • 对象的可变属性,由客户端传递。
    • 例如:文本编辑器中的字符位置。
  3. 享元工厂(Flyweight Factory)

    • 负责创建和管理享元对象,确保共享的内部状态被复用。

享元模式的优点

  1. 减少内存占用
    • 通过共享内部状态,减少重复对象的创建。
  2. 提高性能
    • 减少了对象的创建和销毁开销。
  3. 简化对象管理
    • 享元工厂集中管理共享对象,便于维护。

享元模式的实现

在前端中,享元模式可以通过对象池、缓存或共享数据来实现。以下是几种常见的实现方式。

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)注重的是「事件驱动状态转移」;而状态模式注重的是「状态驱动行为变化」。