JavaScript 设计模式完全指南
设计模式是解决常见软件设计问题的可复用方案,掌握它们能写出更优雅的代码
前言
在设计 JavaScript 应用时,我们经常会遇到一些重复出现的问题:
- 如何确保一个类只有一个实例?
- 如何在不修改原有代码的情况下扩展功能?
- 如何解耦对象之间的依赖关系?
设计模式就是这些问题的解决方案。它们不是代码模板,而是来自大量开发实践的经验总结。
下面结合具体场景讲解 JavaScript 中的核心设计模式,帮助你理解每种模式的适用场景和实现方式。
一、设计模式的本质
什么是设计模式?
设计模式是解决常见软件设计问题的可复用方案。它们具有以下特点:
- 来自实践,经过验证
- 不是代码模板,而是思路和方法论
- 可以提高代码的可维护性和可扩展性
三大分类
| 分类 | 作用 | 代表模式 |
|---|---|---|
| 创建型 | 对象创建机制 | 单例、工厂、建造者 |
| 结构型 | 对象组合方式 | 适配器、装饰器、代理 |
| 行为型 | 对象通信方式 | 观察者、策略、命令 |
现代 JavaScript 的特殊性
ES6+ 的 class、module、Promise 等特性,让某些传统模式有了新的实现方式:
- ES6 Module 天然就是单例
- Promise 简化了回调模式
- class 语法让某些模式更简洁
二、创建型模式
1. 单例模式(Singleton)
核心思想:确保一个类只有一个实例,并提供全局访问点。
场景:全局状态管理、配置对象、数据库连接池。
javascript
// ES6 实现
class Store {
constructor() {
if (Store.instance) {
return Store.instance;
}
this.state = {};
Store.instance = this;
}
getState(key) {
return this.state[key];
}
setState(key, value) {
this.state[key] = value;
}
}
// 使用
const store1 = new Store();
const store2 = new Store();
console.log(store1 === store2); // true
现代替代方案:ES6 Module 天然单例
javascript
// store.js
const state = {};
export function getState(key) {
return state[key];
}
export function setState(key, value) {
state[key] = value;
}
// 导入的永远是同一个实例
2. 工厂模式(Factory)
核心思想:封装对象创建逻辑,根据条件返回不同类型的对象。
场景:需要根据类型创建不同对象、对象创建逻辑复杂。
javascript
// 简单工厂
class Dog {
speak() {
return '汪汪';
}
}
class Cat {
speak() {
return '喵喵';
}
}
function createAnimal(type) {
switch (type) {
case 'dog':
return new Dog();
case 'cat':
return new Cat();
default:
throw new Error('Unknown animal type');
}
}
// 使用
const dog = createAnimal('dog');
console.log(dog.speak()); // 汪汪
优势:
- 创建逻辑集中管理
- 易于扩展新类型
- 调用者不需要知道具体类
3. 建造者模式(Builder)
核心思想:分步骤创建复杂对象,分离构造和表示。
场景:对象有多个可选配置、创建过程复杂。
javascript
class PizzaBuilder {
constructor() {
this.pizza = {};
}
setDough(dough) {
this.pizza.dough = dough;
return this;
}
setSauce(sauce) {
this.pizza.sauce = sauce;
return this;
}
setTopping(topping) {
this.pizza.topping = topping;
return this;
}
build() {
return this.pizza;
}
}
// 使用
const pizza = new PizzaBuilder()
.setDough('thin')
.setSauce('tomato')
.setTopping('cheese')
.build();
console.log(pizza);
// { dough: 'thin', sauce: 'tomato', topping: 'cheese' }
三、结构型模式
1. 适配器模式(Adapter)
核心思想:将一个类的接口转换成客户期望的另一个接口。
场景:复用现有类但接口不兼容、第三方库集成。
javascript
// 旧接口
class OldPayment {
pay(amount) {
console.log(`支付 ${amount} 元(旧方式)`);
}
}
// 新接口期望
class NewPayment {
constructor(adapter) {
this.adapter = adapter;
}
processPayment(amount) {
this.adapter.pay(amount);
}
}
// 适配器
class PaymentAdapter {
constructor(oldPayment) {
this.oldPayment = oldPayment;
}
pay(amount) {
this.oldPayment.pay(amount);
}
}
// 使用
const oldPayment = new OldPayment();
const adapter = new PaymentAdapter(oldPayment);
const newPayment = new NewPayment(adapter);
newPayment.processPayment(100);
2. 装饰器模式(Decorator)
核心思想:动态给对象添加功能,而不改变其结构。
场景:日志记录、权限控制、性能监控。
javascript
// 基础类
class Coffee {
cost() {
return 5;
}
}
// 装饰器
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 2;
}
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1;
}
}
// 使用
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 8
现代替代方案:ES6 装饰器语法(Stage 3)
javascript
function logged(target, context) {
return function(...args) {
console.log(`Calling ${context.name}`);
return target(...args);
};
}
class MyClass {
@logged
myMethod() {
// ...
}
}
3. 代理模式(Proxy)
核心思想:控制对对象的访问,在访问前后添加额外逻辑。
场景:懒加载、访问控制、缓存、日志。
javascript
// 使用 ES6 Proxy
const target = {
name: 'John',
age: 30
};
const handler = {
get(target, prop) {
console.log(`访问属性:${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`设置属性:${prop} = ${value}`);
target[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
// 使用
proxy.name; // 访问属性:name
proxy.age = 31; // 设置属性:age = 31
四、行为型模式
1. 观察者模式(Observer)
核心思想:一对多依赖,一个对象状态改变时自动通知所有依赖者。
场景:事件系统、响应式数据、消息队列。
javascript
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(o => o !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log(`收到通知:${data}`);
}
}
// 使用
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('状态改变');
// 收到通知:状态改变
// 收到通知:状态改变
现代替代方案:RxJS、Vue 响应式系统
2. 策略模式(Strategy)
核心思想:封装可互换的算法,让算法独立于使用它的客户端。
场景:多种排序/验证策略切换、支付方式选择。
javascript
// 策略接口
class PaymentStrategy {
pay(amount) {
throw new Error('Must implement pay()');
}
}
// 具体策略
class CreditCardPayment extends PaymentStrategy {
pay(amount) {
console.log(`信用卡支付 ${amount} 元`);
}
}
class PayPalPayment extends PaymentStrategy {
pay(amount) {
console.log(`PayPal 支付 ${amount} 元`);
}
}
// 上下文
class ShoppingCart {
constructor(paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
setPaymentStrategy(strategy) {
this.paymentStrategy = strategy;
}
checkout(amount) {
this.paymentStrategy.pay(amount);
}
}
// 使用
const cart = new ShoppingCart(new CreditCardPayment());
cart.checkout(100); // 信用卡支付 100 元
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(200); // PayPal 支付 200 元
3. 命令模式(Command)
核心思想:将请求封装为对象,支持撤销、重做等操作。
场景:文本编辑器、宏命令、任务队列。
javascript
// 命令接口
class Command {
execute() {}
undo() {}
}
// 具体命令
class AddTextCommand extends Command {
constructor(editor, text) {
super();
this.editor = editor;
this.text = text;
}
execute() {
this.editor.addText(this.text);
}
undo() {
this.editor.removeText(this.text);
}
}
// 调用者
class Editor {
constructor() {
this.text = '';
this.history = [];
}
addText(text) {
this.text += text;
}
removeText(text) {
this.text = this.text.replace(text, '');
}
executeCommand(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
// 使用
const editor = new Editor();
const command = new AddTextCommand(editor, 'Hello');
editor.executeCommand(command);
console.log(editor.text); // Hello
editor.undo();
console.log(editor.text); // 空
五、模式选择建议
| 问题 | 推荐模式 |
|---|---|
| 确保唯一实例 | 单例模式 |
| 创建逻辑复杂 | 工厂模式、建造者模式 |
| 接口不兼容 | 适配器模式 |
| 动态添加功能 | 装饰器模式 |
| 控制对象访问 | 代理模式 |
| 一对多通知 | 观察者模式 |
| 算法可切换 | 策略模式 |
| 支持撤销重做 | 命令模式 |
使用原则
- 不要过度设计 - 简单问题用简单方案
- 理解后再使用 - 不要为了用模式而用模式
- 结合语言特性 - 利用 ES6+ 新特性简化实现
- 保持代码可读 - 模式应该让代码更清晰,而不是更复杂
六、实战案例
案例:电商购物车系统
背景:电商平台需要实现购物车功能,支持多种支付方式,且购物车状态需要全局唯一。
问题:
- 购物车状态需要在整个应用中保持唯一
- 支付方式可能随时扩展(支付宝、微信、信用卡等)
- 需要解耦购物车逻辑和支付逻辑
实现方案:结合单例模式、策略模式和工厂模式
javascript
// 商品类
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
// 支付策略接口
class PaymentStrategy {
pay(amount) {
throw new Error('Must implement pay()');
}
}
// 具体支付策略
class AlipayPayment extends PaymentStrategy {
pay(amount) {
console.log(`支付宝支付 ${amount} 元`);
}
}
class WechatPayment extends PaymentStrategy {
pay(amount) {
console.log(`微信支付 ${amount} 元`);
}
}
// 购物车(单例模式)
class ShoppingCart {
constructor() {
if (ShoppingCart.instance) {
return ShoppingCart.instance;
}
this.items = [];
this.paymentStrategy = null;
ShoppingCart.instance = this;
}
addItem(product) {
this.items.push(product);
}
setPaymentStrategy(strategy) {
this.paymentStrategy = strategy;
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
checkout() {
if (!this.paymentStrategy) {
throw new Error('请选择支付方式');
}
this.paymentStrategy.pay(this.getTotal());
this.items = [];
}
}
// 使用
const cart = new ShoppingCart();
cart.addItem(new Product('商品 A', 100));
cart.addItem(new Product('商品 B', 200));
cart.setPaymentStrategy(new AlipayPayment());
cart.checkout(); // 支付宝支付 300 元
效果:
- 购物车全局唯一,避免状态不一致
- 支付方式可灵活切换,无需修改购物车代码
- 新增支付方式只需扩展 PaymentStrategy,符合开闭原则
要点:
- 单例模式确保购物车状态唯一
- 策略模式实现支付方式灵活切换
- 依赖倒置让系统更易扩展
总结
设计模式是开发者工具箱中的重要武器,但使用时需要注意:
- 理解问题本质 - 先理解要解决的问题,再选择合适的模式
- 不要过度设计 - 简单问题用简单方案
- 结合语言特性 - 利用 JavaScript 的特性简化实现
- 保持代码可读 - 模式应该让代码更清晰
掌握这些模式,能让你在面对复杂问题时游刃有余,写出更优雅、可维护的代码。
参考资料
- patterns.dev - JavaScript 设计模式:www.patterns.dev/
- 《JavaScript 设计模式》- Addy Osmani
- Refactoring.Guru - 设计模式教程:refactoring.guru/design-patt...
- ES6 装饰器提案:github.com/tc39/propos...
- MDN - Proxy: developer.mozilla.org/zh-CN/docs/...
觉得文章对你有帮助?欢迎点赞收藏,分享给更多需要的朋友!