返回博客
技术 2025年3月5日 9 分钟阅读 · 2208 字

JavaScript 设计模式实战:单例、观察者、策略与装饰器

用现代 JavaScript 实现经典设计模式,提升代码可维护性与可扩展性

#JavaScript #设计模式 #代码架构
本文由 AI 辅助生成,经人工审核发布

设计模式是前人总结的代码组织经验,是解决特定场景下重复问题的模板。本文用现代 JavaScript(ES6+)实现五种经典设计模式,并说明其适用场景与注意事项。

一、单例模式

1.1 意图

保证一个类只有一个实例,并提供全局访问点。适用于全局配置、缓存、日志等需要唯一实例的场景。

1.2 ES6+ 实现

class Singleton {
  static #instance = null;

  constructor(config) {
    if (Singleton.#instance) {
      return Singleton.#instance;
    }
    this.config = config;
    this.createdAt = Date.now();
    Singleton.#instance = this;
  }

  static getInstance(config = {}) {
    if (!Singleton.#instance) {
      Singleton.#instance = new Singleton(config);
    }
    return Singleton.#instance;
  }

  getConfig(key) {
    return this.config[key];
  }
}

const a = new Singleton({ apiUrl: '/api' });
const b = new Singleton({ apiUrl: '/other' });
console.log(a === b);          // true
console.log(a.getConfig('apiUrl')); // '/api'(第一次的配置)

1.3 更简洁的模块单例

// config.js —— 利用 ES Module 天然单例特性
const config = {
  apiUrl: '/api',
  timeout: 5000,
  retry: 3
};

export default Object.freeze(config); // 冻结,防止意外修改

1.4 适用场景与陷阱

场景说明
全局配置应用启动时加载,全程只读
日志服务统一输出入口
数据库连接连接池复用
状态管理Redux store 本质是单例

陷阱:单例引入全局状态,增加耦合度,使单元测试困难。谨慎使用,优先考虑依赖注入。

二、观察者模式与发布订阅模式

2.1 意图

定义对象间一对多的依赖关系,当对象状态变化时,所有依赖者自动收到通知。

2.2 观察者模式实现

class Subject {
  #observers = new Set();

  subscribe(observer) {
    this.#observers.add(observer);
    return () => this.#observers.delete(observer); // 返回取消订阅函数
  }

  notify(data) {
    this.#observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`${this.name} 收到更新:`, data);
  }
}

// 使用
const news = new Subject();
const reader1 = new Observer('读者A');
const reader2 = new Observer('读者B');

const unsub1 = news.subscribe(reader1);
news.subscribe(reader2);

news.notify('头条:ES14 新特性发布');
// 读者A 收到更新: 头条:ES14 新特性发布
// 读者B 收到更新: 头条:ES14 新特性发布

unsub1(); // 读者A 取消订阅
news.notify('第二条新闻');
// 仅 读者B 收到

2.3 发布订阅模式

发布订阅与观察者的区别:多了一个”事件中心”作为中间层,发布者和订阅者互不感知。

class EventBus {
  #events = new Map();

  on(event, callback) {
    if (!this.#events.has(event)) {
      this.#events.set(event, new Set());
    }
    this.#events.get(event).add(callback);
    return () => this.off(event, callback);
  }

  off(event, callback) {
    this.#events.get(event)?.delete(callback);
  }

  emit(event, data) {
    this.#events.get(event)?.forEach(cb => cb(data));
  }

  once(event, callback) {
    const unsubscribe = this.on(event, (data) => {
      callback(data);
      unsubscribe();
    });
    return unsubscribe;
  }
}

const bus = new EventBus();

const unsub = bus.on('user:login', (user) => {
  console.log('用户登录:', user.name);
});

bus.emit('user:login', { name: 'Alice' });
// 用户登录: Alice

unsub();
bus.emit('user:login', { name: 'Bob' }); // 无输出

2.4 两者对比

维度观察者模式发布订阅模式
结构Subject → ObserverPublisher → Bus → Subscriber
耦合度较高(直接通知)较低(通过事件中心)
适用场景数据双向绑定跨组件通信、模块解耦

2.5 实际应用

  • Vue 响应式系统:数据变化时通知视图更新
  • Node.js EventEmitteremitter.on('event', cb) / emitter.emit('event')
  • 浏览器事件addEventListener 本质是观察者模式

三、策略模式

3.1 意图

定义一系列算法,将每个算法封装起来,使它们可以互换。策略模式让算法的变化独立于使用算法的客户端。

3.2 实现

// 定义策略集合
const pricingStrategies = {
  normal: (price) => price,

  member: (price) => price * 0.9,       // 会员九折

  vip: (price) => price * 0.8,          // VIP 八折

  fullReduction: (price) => {           // 满 200 减 30
    return price >= 200 ? price - 30 : price;
  },

  coupon: (price, coupon) => {          // 优惠券
    return Math.max(0, price - coupon);
  }
};

// 上下文
class PriceCalculator {
  #strategy;

  constructor(strategy = 'normal') {
    this.setStrategy(strategy);
  }

  setStrategy(name) {
    if (!pricingStrategies[name]) {
      throw new Error(`未知策略: ${name}`);
    }
    this.#strategy = pricingStrategies[name];
  }

  calculate(price, ...args) {
    return this.#strategy(price, ...args);
  }
}

// 使用
const calc = new PriceCalculator('normal');
console.log(calc.calculate(250));  // 250

calc.setStrategy('member');
console.log(calc.calculate(250));  // 225

calc.setStrategy('fullReduction');
console.log(calc.calculate(250));  // 220

calc.setStrategy('coupon');
console.log(calc.calculate(250, 50)); // 200

3.3 用 Map 简化表驱动

const validators = new Map([
  ['required', (v) => v !== '' && v != null],
  ['email', (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)],
  ['minLength', (v, n) => String(v).length >= n],
  ['maxValue', (v, max) => Number(v) <= max],
]);

function validate(value, rules) {
  return rules.every(({ rule, params = [] }) =>
    validators.get(rule)(value, ...params)
  );
}

validate('test@example.com', [
  { rule: 'required' },
  { rule: 'email' }
]); // true

3.4 适用场景

  • 表单验证规则
  • 价格计算/折扣策略
  • 排序算法切换
  • 权限控制策略

策略模式消除了大量的 if/else,将每个策略独立封装,新增策略时不修改已有代码(开闭原则)。

四、装饰器模式

4.1 意图

动态地给对象添加额外职责,而不改变其接口。装饰器是继承的灵活替代方案。

4.2 函数装饰器实现

// 日志装饰器
function withLog(fn, name = fn.name) {
  return function(...args) {
    console.log(`[${name}] 调用,参数:`, args);
    const result = fn.apply(this, args);
    console.log(`[${name}] 返回:`, result);
    return result;
  };
}

// 性能计时装饰器
function withTiming(fn, name = fn.name) {
  return function(...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    const end = performance.now();
    console.log(`[${name}] 耗时: ${(end - start).toFixed(2)}ms`);
    return result;
  };
}

// 缓存装饰器
function withCache(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log('命中缓存');
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// 组合装饰器
function compose(...decorators) {
  return (fn) => decorators.reduceRight((f, dec) => dec(f), fn);
}

// 使用
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const enhancedFib = compose(
  withLog,
  withTiming,
  withCache
)(fibonacci);

enhancedFib(10);

4.3 类方法装饰器(ES2023)

// 装饰器函数签名
function log(target, context) {
  const originalMethod = target;
  function replacement(...args) {
    console.log(`调用 ${context.name},参数:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${context.name} 返回:`, result);
    return result;
  }
  return replacement;
}

class APIClient {
  @log
  async fetchUser(id) {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  }

  @log
  async createUser(data) {
    const res = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(data)
    });
    return res.json();
  }
}

4.4 适用场景

  • 日志记录、性能监控
  • 权限校验
  • 缓存
  • 重试机制
// 重试装饰器
function withRetry(fn, { times = 3, delay = 1000 } = {}) {
  return async function(...args) {
    let lastError;
    for (let i = 0; i < times; i++) {
      try {
        return await fn.apply(this, args);
      } catch (err) {
        lastError = err;
        console.log(`第 ${i + 1} 次失败,${delay}ms 后重试`);
        await new Promise(r => setTimeout(r, delay));
      }
    }
    throw lastError;
  };
}

const fetchWithRetry = withRetry(fetch, { times: 3, delay: 500 });

五、工厂模式与模块模式

5.1 简单工厂

class Button {
  constructor({ text, color, size }) {
    this.text = text;
    this.color = color;
    this.size = size;
  }
  render() {
    console.log(`渲染按钮: ${this.text} (${this.color}, ${this.size})`);
  }
}

class ButtonFactory {
  static create(type) {
    const presets = {
      primary: { text: '确认', color: 'blue', size: 'medium' },
      danger:  { text: '删除', color: 'red', size: 'medium' },
      ghost:   { text: '取消', color: 'gray', size: 'small' },
    };
    const config = presets[type] ?? presets.primary;
    return new Button(config);
  }
}

const btn = ButtonFactory.create('danger');
btn.render(); // 渲染按钮: 删除 (red, medium)

5.2 抽象工厂

// 抽象工厂:创建相关产品族
class UIFactory {
  createButton() { throw new Error('未实现'); }
  createInput() { throw new Error('未实现'); }
}

class MaterialFactory extends UIFactory {
  createButton() { return { type: 'Material Button', theme: 'material' }; }
  createInput() { return { type: 'Material Input', theme: 'material' }; }
}

class AntDesignFactory extends UIFactory {
  createButton() { return { type: 'AntD Button', theme: 'antd' }; }
  createInput() { return { type: 'AntD Input', theme: 'antd' }; }
}

function renderUI(factory) {
  const button = factory.createButton();
  const input = factory.createInput();
  console.log(button, input);
}

renderUI(new MaterialFactory());
renderUI(new AntDesignFactory());

5.3 模块模式

ES Module 天然是模块模式的最佳实现:

// user-service.js
const privateState = new WeakMap();

class UserService {
  constructor() {
    privateState.set(this, { cache: new Map() });
  }

  async getUser(id) {
    const state = privateState.get(this);
    if (state.cache.has(id)) return state.cache.get(id);

    const res = await fetch(`/api/users/${id}`);
    const user = await res.json();
    state.cache.set(id, user);
    return user;
  }
}

export default new UserService(); // 单例导出
export const VERSION = '1.0.0';    // 常量导出

六、模式对比与选择

模式解决的问题关键特征常见场景
单例全局唯一实例私有构造 + 静态获取配置、连接池
观察者一对多通知Subject 直接通知数据绑定、事件系统
发布订阅解耦的跨模块通信通过事件中心中转消息总线、组件通信
策略算法可互换封装策略 + 上下文切换验证、计价、排序
装饰器动态扩展功能包裹原函数/类日志、缓存、重试
工厂封装对象创建集中创建逻辑UI 组件、复杂对象
模块封装与访问控制私有 + 公开接口服务层、工具库

七、设计原则

设计模式的背后是 SOLID 原则:

原则缩写含义
单一职责SRP一个类/函数只做一件事
开闭原则OCP对扩展开放,对修改关闭
里氏替换LSP子类能替换父类使用
接口隔离ISP不依赖不需要的接口
依赖倒置DIP依赖抽象,不依赖具体实现
  • 策略模式体现了开闭原则——新增策略不改已有代码
  • 观察者模式体现了依赖倒置——Subject 依赖 Observer 抽象接口
  • 装饰器模式体现了单一职责——每个装饰器只关注一个横切关注点

总结

设计模式不是教条,而是工具。使用时遵循:

  1. 不过度设计:简单场景用 if/else 足够,不要硬套模式
  2. 理解意图:每个模式解决的问题比实现细节更重要
  3. 结合语言特性:ES6+ 的 class、Proxy、Module 让许多模式更简洁
  4. 关注可测试性:好的模式天然提升代码可测试性
  5. 适时重构:当重复代码出现三次以上,考虑用模式抽象

模式是手段,可维护、可扩展的代码才是目的。在实际项目中灵活运用,而非为了模式而模式。