万字长文解析前端常用设计模式

60 阅读25分钟

一、设计原则

1.单一职责原则-Single Responsibility Principle-SRP

我们知道,一套功能完备的软件系统可能是非常复杂的。既然要利用好面向对象的思想,那么对一个大系统的拆分、模块化是不可或缺的软件设计步骤。面向对象以“类”来划分模块边界,再以“方法”来分隔其功能。我们可以将某业务功能划归到一个类中,也可以拆分为几个类分别实现,但是不管对其负责的业务范围大小做怎样的权衡与调整,这个类的角色职责应该是单一的,或者其方法所完成的功能也应该是单一的。总之,不是自己分内之事绝不该负责,这就是单一职责原则(Single Responsibility Principle)​。符合单一职责原则的设计能使类具备“高内聚性”​,让单个模块变得“简单”​“易懂”​,如此才能增强代码的可读性与可复用性,并提高系统的易维护性与易测试性。

不符合单一职责原则的代码场景 :多功能混合的用户信息组件(React 为例)

假设一个UserProfile组件同时承担了数据请求、UI 渲染、表单提交、错误处理4 项职责:

// 违反单一职责的组件
function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [editForm, setEditForm] = useState({ name: '', age: '' });

  // 职责1:请求用户数据(数据获取)
  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/user');
        const data = await res.json();
        setUser(data);
        setEditForm({ name: data.name, age: data.age }); // 初始化表单
      } catch (err) {
        setError('加载失败:' + err.message); // 职责4:错误处理
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, []);

  // 职责3:处理表单提交(数据修改)
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    try {
      await fetch('/api/user', {
        method: 'PUT',
        body: JSON.stringify(editForm),
      });
      setUser(editForm); // 更新本地数据
    } catch (err) {
      setError('提交失败:' + err.message); // 职责4:错误处理
    } finally {
      setLoading(false);
    }
  };

  // 职责2:UI渲染(包含加载、错误、正常、编辑状态)
  if (loading) return <div>加载中...</div>;
  if (error) return <div className="error">{error}</div>;
  if (!user) return null;

  return (
    <div className="user-profile">
      <h2>用户信息</h2>
      <form onSubmit={handleSubmit}>
        <input
          name="name"
          value={editForm.name}
          onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
        />
        <input
          name="age"
          value={editForm.age}
          onChange={(e) => setEditForm({ ...editForm, age: e.target.value })}
        />
        <button type="submit">保存</button>
      </form>
    </div>
  );
}

问题分析:

这个组件存在 4 个可能导致变更的原因:

  • 数据请求逻辑变更(如接口地址、参数调整);
  • UI 渲染样式 / 结构变更(如增加头像展示);
  • 表单提交逻辑变更(如增加字段验证);
  • 错误处理方式变更(如从文字提示改为弹窗)。

任何一个变更都可能影响其他不相关的逻辑,维护成本高。

改造:按职责拆分

将 4 项职责拆分为4 个独立单元,通过组合实现功能:

  1. 数据请求职责:抽离为 API 服务(单独模块)
// services/userApi.js(仅负责数据请求)
export const userApi = {
  fetchUser: async () => {
    const res = await fetch('/api/user');
    if (!res.ok) throw new Error('加载失败');
    return res.json();
  },
  updateUser: async (data) => {
    const res = await fetch('/api/user', {
      method: 'PUT',
      body: JSON.stringify(data),
    });
    if (!res.ok) throw new Error('提交失败');
    return data;
  },
};
  1. 错误处理职责:抽离为通用 Hook(复用逻辑)
// hooks/useError.js(仅负责错误状态管理)
export function useError() {
  const [error, setError] = useState('');
  const setErrorMsg = (msg) => setError(msg);
  const clearError = () => setError('');
  return { error, setErrorMsg, clearError };
}
  1. 表单处理职责:抽离为表单组件(专注表单逻辑)
// components/UserForm.js(仅负责表单渲染和数据收集)
function UserForm({ initialData, onSubmit, loading }) {
  const [formData, setFormData] = useState(initialData);

  const handleChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  return (
    <form onSubmit={(e) => onSubmit(e, formData)}>
      <input name="name" value={formData.name} onChange={handleChange} />
      <input name="age" value={formData.age} onChange={handleChange} />
      <button type="submit" disabled={loading}>保存</button>
    </form>
  );
}
  1. UI 渲染职责:主组件仅负责组合与状态协调
// 符合单一职责的UserProfile(仅负责协调各模块,不做具体逻辑)
function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const { error, setErrorMsg, clearError } = useError();

  // 调用API服务加载数据
  const loadUser = async () => {
    setLoading(true);
    clearError();
    try {
      const data = await userApi.fetchUser();
      setUser(data);
    } catch (err) {
      setErrorMsg(err.message);
    } finally {
      setLoading(false);
    }
  };

  // 调用API服务提交数据
  const handleFormSubmit = async (e, formData) => {
    e.preventDefault();
    setLoading(true);
    clearError();
    try {
      await userApi.updateUser(formData);
      setUser(formData);
    } catch (err) {
      setErrorMsg(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { loadUser(); }, []);

  // 仅负责渲染状态协调
  if (loading) return <div>加载中...</div>;
  if (error) return <div className="error">{error}</div>;
  if (!user) return null;

  return (
    <div className="user-profile">
      <h2>用户信息</h2>
      <UserForm 
        initialData={user} 
        onSubmit={handleFormSubmit} 
        loading={loading} 
      />
    </div>
  );
}

2.开放封闭原则-OpenClosed Principle -OCP

其中“开”指的是对扩展开放,而“闭”则指的是对修改关闭。简单来讲就是不要修改已有的代码,而要去编写新的代码。这对于已经上线并运行稳定的软件项目尤为重要。修改代码的代价是巨大的,小小一个修改有可能会造成整个系统瘫痪,因为其可能会波及的地方是不可预知的,这给测试工作也带来了很大的挑战。

举个例子,我们设计了一个集成度很高的计算机主板,各种部件如CPU、内存、硬盘一应俱全,该有的都已集成了,大而全的设计看似不需要再进行扩展了。然而当用户需要安装一个摄像头的时候,我们不得不拆开机箱对内部电路进行二次修改,并加装摄像头。在满足用户的各种需求后,主板会被修改得面目全非,各种导线焊点杂乱无章。如果当初设计主板的时候预留好接口,用户就能自由地扩展外设了,想用什么就接入什么,如用户可以购入摄像头、U盘等外设并插入主板的USB接口,而主板则被封装于机箱中,不再需要做任何更改,这便是对扩展的开放,以及对修改的关闭。

当系统升级时,如果为了增强系统功能而需要进行大量的代码修改,则说明这个系统的设计是失败的,是违反开闭原则的。反之,对系统的扩展应该只需添加新的软件模块,系统模式一旦确立就不再修改现有代码,这才是符合开闭原则的优雅设计。

const Input = ({ type }) => (
    <div>
        {type === 'name' && <span>姓名</span>}
        {type === 'phone' && <span>+86</span>}
        <input type="text" />
        {type === 'money' && <span></span>}
    </div>
)


const InputOCP = ({ prefix, suffix } ) => {
    return (
        <div>
            {prefix && prefix()}
            <input type="text" />
            {suffix && suffix()}
        </div>
    )

}

  
function App() {
    return (
        <div>
            <Input type="name" />
            <Input type="phone" />
            <Input type="money" />

            <hr/>

            <InputOCP prefix={() => <span>姓名</span>} />
            <InputOCP prefix={() => <span>+86</span>} />
            <InputOCP suffix={() => <span></span>} />
        </div>
    )
}

3.里氏替换原则-Liskov Substitution Principle-LSP

此原则指的是在任何父类出现的地方子类也一定可以出现,也就是说一个优秀的软件设计中有引用父类的地方,一定也可以替换为其子类。其实面向对象设计语言的特性“继承与多态”正是为此而生。我们在设计的时候一定要充分利用这一特性,写框架代码的时候要面向接口编程,而不是深入到具体子类中去,这样才能保证子类多态替换的可能性。

假设我们定义一个“禽类”​,给它加一个飞翔方法fly(),我们就可以自由地继承禽类衍生出各种鸟儿,并轻松自如地调用其飞翔方法。如果某天需要鸵鸟加入禽类的行列,鸵鸟可以继承禽类,这没有任何问题,但鸵鸟不会飞,那么飞翔方法fly()就显得多余了,而且在所有禽类出现的地方无法用鸵鸟进行替换,这便违反了里氏替换原则。不是所有禽类都能飞,也不是所有兽类都只能走。

经过反思,我们意识到最初的设计是有问题的,因为“禽类”与“飞翔”并无必然关系,所以对于禽类不应该定义飞翔方法fly()。接着,我们对高层抽象进行重构,把禽类的飞翔方法fly()抽离出去并单独定义一个飞翔接口Flyable,对于有飞翔能力的鸟儿可以继承禽类并同时实现飞翔接口,而对于鸵鸟则依然继承禽类,但不用去实现飞翔接口。再比如蝙蝠不是鸟儿但可以飞,那么它应该继承自兽类,并实现飞翔接口。这样一来,是否是鸟儿取决于是否继承自禽类,而能不能飞要取决于是否实现了飞翔接口。所有禽类出现的地方我们都可以用子类进行替换,所有飞翔接口出现的地方则可以被替换为其实现,如蝙蝠、蜜蜂,甚至是飞机。所以优秀的软件设计一定要有合理的定义与规划,这样才能容许软件可扩展,使任何子类实现都能在其高层抽象的定义范围内自由替换,且不引发任何系统问题。

4.接口隔离原则-Interface Segregation Principle -ISP

简单来说,就是切勿将接口定义成全能型的,否则实现类就必须神通广大,这样便丧失了子类实现的灵活性,降低了系统的向下兼容性。反之,定义接口的时候应该尽量拆分成较小的粒度,往往一个接口只对应一个职能。

其实接口隔离原则与单一职责原则如出一辙,只不过前者是对高层行为能力的一种单一职责规范,这非常好理解,分开的容易合起来,但合起来的就不容易分开了。接口隔离原则能很好地避免了过度且臃肿的接口设计,轻量化的接口不会造成对实现类的污染,使系统模块的组装变得更加灵活。

违背接口隔离原则的设计:

class Vehicle {
    drive() {
        console.log('Driving...')
    }

    fly() {
        console.log('Flying...')
    }
}

  

class Car extends Vehicle {
    drive() {
        console.log('I am car, I can drive')
    }

    fly() {
        console.log('I am car, I can not fly')
    }
}

  
class Airplane extends Vehicle {
    drive() {
        console.log('I am airplane, I can not drive')
    }

    fly() {
        console.log('I am airplane, I can fly')
    }
}

将接口进行细化:

class Vehicle {

}

class VehicleCanDrive extends Vehicle {
    drive() {
        console.log('Driving...')
    }
}

class VehicleCanFly extends Vehicle {
    fly() {
        console.log('Flying...')
    }
}

class Car extends VehicleCanDrive {
    drive() {
        console.log('I am car, I can drive')
    }
}

class Airplane extends VehicleCanFly {
    fly() {
        console.log('I am airplane, I can fly')
    }
}

5.依赖倒转原则-DependencyInversion Principle-DIP

我们知道,面向对象中的依赖是类与类之间的一种关系,如H(高层)类要调用L(底层)类的方法,我们就说H类依赖L类。依赖倒置原则(Dependency Inversion Principle)指高层模块不依赖底层模块,也就是说高层模块只依赖上层抽象,而不直接依赖具体的底层实现,从而达到降低耦合的目的。如上面提到的H与L的依赖关系必然会导致它们的强耦合,也许L任何细枝末节的变动都可能影响H,这是一种非常死板的设计。而依赖倒置的做法则是反其道而行,我们可以创建L的上层抽象A,然后H即可通过抽象A间接地访问L,那么高层H不再依赖底层L,而只依赖上层抽象A。这样一来系统会变得更加松散,这也印证了我们在“里氏替换原则”中所提到的“面向接口编程”​,以达到替换底层实现的目的。

举个例子,公司总经理制订了下一年度的目标与计划,为了提高办公效率,总经理决定年底要上线一套全新的办公自动化软件。那么总经理作为发起方该如何实施这个计划呢?直接发动基层程序员并调用他们的研发方法吗?我想世界上没有以这种方式管理公司的领导吧。公司高层一定会发动IT部门的上层抽象去执行,调用IT部门经理的work方法并传入目标即可,至于这个work方法的具体实现者也许是架构师甲,也可能是程序员乙,总经理也许根本不认识他们,这就达到了公司高层与底层员工实现解耦的目的。这就是将“高层依赖底层”倒置为“底层依赖高层”的好处。

违背依赖倒转原则的设计:

// 高层模块
class NotificationService {
    constructor() {
        this.emailSender = new EmailSender()
        this.imSender = new IMSender()
    }

    sendEmail(message) {
        this.emailSender.send(message)
    }
    
    sendIM(message) {
        this.imSender.send(message)
    }
}

// 底层模块
class EmailSender {
    send(message) {
        console.log(`Using email to send message: ${message}`)
    }
}

class IMSender {
    send(message) {
        console.log(`Using IM to send message: ${message}`)
    }
}

让高层模块依赖于抽象:

// 高层模块
class NotificationService {
    constructor(sender) {
        this.sender = sender
    }
    
    // 没有在send方法中去明确具体的实现细节,而只有抽象
    send(message) {
        this.sender.send(message)
    }
}

class MessageSender {
    send() {
        throw new Error('This method should be implemented')
    }
}

// 底层模块
class EmailSender extends MessageSender {
    send(message) {
        console.log(`Using email to send message: ${message}`)
    }
}

class IMSender extends MessageSender {
    send(message) {
        console.log(`Using IM to send message: ${message}`)
    }
}

new NotificationService(new EmailSender()).send('Hello, world!')

二、设计模式

1.创建型

1.1建造者模式

建造者模式(Builder Pattern)是一种创建型设计模式,核心思想是:将复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的对象表示。

简单说,就是把一个复杂对象的 “组装步骤” 拆解开,通过一步步设置细节,最终构建出完整对象。它适合处理那些具有多个可选组件、配置项复杂的对象创建场景。

核心角色

  1. 产品(Product) :最终要创建的复杂对象(如一个复杂的 UI 组件、配置对象等)。
  2. 建造者(Builder) :定义构建产品的抽象接口,包含一系列设置产品各部分的方法(如setA()setB())和一个返回最终产品的方法(如build())。
  3. 具体建造者(Concrete Builder) :实现建造者接口,负责具体的构建逻辑(如何设置每个部分)。
  4. 指挥者(Director) :可选角色,负责控制构建流程(按固定步骤调用建造者的方法),简化客户端调用。

前端示例:构建复杂弹窗(Modal)组件

假设需要实现一个弹窗组件,它有多个可选配置:标题、内容、确认按钮、取消按钮、关闭按钮、动画效果、尺寸等。如果用常规方式通过构造函数或配置对象创建,可能会出现参数过多、默认值混乱的问题。用建造者模式可以优雅解决。

  1. 定义 “产品”:弹窗类(Modal)
// 产品:弹窗对象(最终要创建的复杂对象)
class Modal {
  constructor() {
    this.title = ''; // 标题
    this.content = ''; // 内容
    this.hasConfirmBtn = false; // 是否有确认按钮
    this.confirmText = '确认'; // 确认按钮文本
    this.hasCancelBtn = false; // 是否有取消按钮
    this.cancelText = '取消'; // 取消按钮文本
    this.hasCloseBtn = true; // 是否有关闭按钮
    this.animation = 'fade'; // 动画效果(fade/slide)
    this.size = 'medium'; // 尺寸(small/medium/large)
  }

  // 渲染弹窗到页面
  render() {
    const modal = document.createElement('div');
    modal.className = `modal ${this.animation} ${this.size}`;
    modal.innerHTML = `
      <div class="modal-header">
        <h3>${this.title}</h3>
        ${this.hasCloseBtn ? '<span class="close">×</span>' : ''}
      </div>
      <div class="modal-body">${this.content}</div>
      <div class="modal-footer">
        ${this.hasCancelBtn ? `<button class="cancel">${this.cancelText}</button>` : ''}
        ${this.hasConfirmBtn ? `<button class="confirm">${this.confirmText}</button>` : ''}
      </div>
    `;
    document.body.appendChild(modal);
    return modal;
  }
}
  1. 定义 “建造者” 接口及具体实现

建造者负责分步设置弹窗的各个部分,通过链式调用让配置更直观:

// 建造者:定义构建弹窗的接口
class ModalBuilder {
  constructor() {
    this.modal = new Modal(); // 初始化产品
  }

  // 设置标题
  setTitle(title) {
    this.modal.title = title;
    return this; // 链式调用
  }

  // 设置内容
  setContent(content) {
    this.modal.content = content;
    return this;
  }

  // 开启确认按钮
  withConfirmBtn(text = '确认') {
    this.modal.hasConfirmBtn = true;
    this.modal.confirmText = text;
    return this;
  }

  // 开启取消按钮
  withCancelBtn(text = '取消') {
    this.modal.hasCancelBtn = true;
    this.modal.cancelText = text;
    return this;
  }

  // 关闭关闭按钮
  withoutCloseBtn() {
    this.modal.hasCloseBtn = false;
    return this;
  }

  // 设置动画
  setAnimation(animation) {
    this.modal.animation = animation;
    return this;
  }

  // 设置尺寸
  setSize(size) {
    this.modal.size = size;
    return this;
  }

  // 构建并返回最终产品
  build() {
    return this.modal;
  }
}
  1. (可选)定义 “指挥者”:封装固定构建流程

如果有一些常用的弹窗模板(如 “确认弹窗”“提示弹窗”),可以用指挥者封装固定步骤:

// 指挥者:负责固定流程的构建(简化重复配置)
class ModalDirector {
  // 创建“确认弹窗”模板(有标题、内容、确认+取消按钮)
  static buildConfirmModal(builder, title, content) {
    return builder
      .setTitle(title)
      .setContent(content)
      .withConfirmBtn()
      .withCancelBtn()
      .setSize('small')
      .build();
  }

  // 创建“提示弹窗”模板(只有标题、内容、关闭按钮)
  static buildAlertModal(builder, title, content) {
    return builder
      .setTitle(title)
      .setContent(content)
      .withoutCloseBtn() // 示例:隐藏默认关闭按钮(实际可能保留)
      .setAnimation('slide')
      .build();
  }
}
  1. 客户端使用:灵活构建弹窗
// 场景1:自定义构建一个弹窗
const customModal = new ModalBuilder()
  .setTitle('自定义弹窗')
  .setContent('这是一个完全自定义的弹窗')
  .withConfirmBtn('提交')
  .withoutCloseBtn()
  .setAnimation('slide')
  .setSize('large')
  .build();
customModal.render();

// 场景2:使用指挥者创建确认弹窗
const confirmBuilder = new ModalBuilder();
const confirmModal = ModalDirector.buildConfirmModal(
  confirmBuilder,
  '删除确认',
  '确定要删除这条数据吗?'
);
confirmModal.render();

// 场景3:使用指挥者创建提示弹窗
const alertBuilder = new ModalBuilder();
const alertModal = ModalDirector.buildAlertModal(
  alertBuilder,
  '操作成功',
  '数据已保存'
);
alertModal.render();

为什么这样设计符合建造者模式?

  1. 分离构建与表示Modal 类只负责 “是什么”(产品的结构),ModalBuilder 负责 “怎么建”(分步配置),两者解耦。
  2. 灵活组合:通过链式调用可以自由搭配配置项,避免了 “传递大量可选参数” 的问题(如 new Modal(title, content, true, '确认', false, ...))。
  3. 易于扩展:如果需要新增弹窗配置(如 “是否可拖拽”),只需在 Modal 中加属性,在 ModalBuilder 中加对应方法,不影响已有逻辑。

1.2工厂模式

工厂模式(Factory Pattern)是一种创建型设计模式,核心思想是:通过一个统一的 “工厂” 接口来创建对象,将对象的创建逻辑与使用逻辑分离,无需暴露对象的具体创建细节(如具体类名、构造过程)。

简单说,就是 “用一个专门的类 / 函数负责创建其他类的实例”,使用者只需告诉工厂 “想要什么”,无需关心 “怎么造出来”。

核心价值

  • 隐藏创建细节:使用者不需要知道对象的具体类名或构造逻辑,只需调用工厂方法。
  • 降低耦合:当对象的创建逻辑变化时(如换一个实现类),只需修改工厂,无需修改所有使用该对象的地方。
  • 便于扩展:新增对象类型时,只需扩展工厂逻辑,符合 “开闭原则”。

前端开发中,根据复杂度不同,工厂模式可分为三种形式:

  1. 简单工厂模式(Simple Factory)

最基础的形式:由一个工厂类 / 函数根据输入参数直接创建并返回不同类型的对象。

适用场景:创建的对象类型较少、逻辑简单,且不会频繁新增类型。

前端示例:创建不同类型的提示框(Toast)

假设需要实现三种提示框:成功提示(success)、错误提示(error)、警告提示(warning),它们样式和图标不同,但都有show()方法。

// 1. 定义产品类(具体的提示框)
class SuccessToast {
  constructor(message) {
    this.message = message;
    this.type = 'success';
    this.icon = '✅';
  }

  show() {
    console.log(`${this.icon} [${this.type}]: ${this.message}`);
  }
}

class ErrorToast {
  constructor(message) {
    this.message = message;
    this.type = 'error';
    this.icon = '❌';
  }

  show() {
    console.log(`${this.icon} [${this.type}]: ${this.message}`);
  }
}

class WarningToast {
  constructor(message) {
    this.message = message;
    this.type = 'warning';
    this.icon = '⚠️';
  }

  show() {
    console.log(`${this.icon} [${this.type}]: ${this.message}`);
  }
}

// 2. 简单工厂:根据类型参数创建对应实例
class ToastFactory {
  static createToast(type, message) {
    switch (type) {
      case 'success':
        return new SuccessToast(message);
      case 'error':
        return new ErrorToast(message);
      case 'warning':
        return new WarningToast(message);
      default:
        throw new Error(`不支持的提示类型:${type}`);
    }
  }
}

// 3. 使用者调用(无需关心具体类,只依赖工厂)
const successToast = ToastFactory.createToast('success', '操作成功');
successToast.show(); // 输出:✅ [success]: 操作成功

const errorToast = ToastFactory.createToast('error', '操作失败');
errorToast.show(); // 输出:❌ [error]: 操作失败

优势:使用者只需调用ToastFactory.createToast(类型, 消息),无需知道SuccessToast等具体类,创建逻辑集中在工厂中。

  1. 工厂方法模式(Factory Method)

当创建的对象类型较多或需要频繁扩展时,简单工厂的switch逻辑会越来越臃肿。工厂方法模式将 “创建逻辑” 延迟到子类工厂中,每个子类工厂只负责创建一种类型的对象。

核心结构

  • 抽象工厂接口:定义创建对象的方法(如create())。
  • 具体工厂类:实现抽象工厂接口,负责创建特定类型的对象。

前端示例:扩展提示框工厂(支持新增类型)

如果后续需要新增 “信息提示(info)”,用工厂方法模式可避免修改原有工厂逻辑。

// 1. 抽象产品接口(所有提示框都需实现show方法)
class Toast {
  show() {
    throw new Error('子类必须实现show方法');
  }
}

// 2. 具体产品类(继承抽象产品)
class SuccessToast extends Toast {
  constructor(message) { this.message = message; this.icon = '✅'; }
  show() { console.log(`${this.icon} 成功:${this.message}`); }
}

class ErrorToast extends Toast {
  constructor(message) { this.message = message; this.icon = '❌'; }
  show() { console.log(`${this.icon} 错误:${this.message}`); }
}

// 新增:信息提示产品
class InfoToast extends Toast {
  constructor(message) { this.message = message; this.icon = 'ℹ️'; }
  show() { console.log(`${this.icon} 信息:${this.message}`); }
}

// 3. 抽象工厂接口(定义创建方法)
class ToastFactory {
  createToast(message) {
    throw new Error('子类必须实现createToast方法');
  }
}

// 4. 具体工厂类(每个工厂对应一种产品)
class SuccessToastFactory extends ToastFactory {
  createToast(message) {
    return new SuccessToast(message);
  }
}

class ErrorToastFactory extends ToastFactory {
  createToast(message) {
    return new ErrorToast(message);
  }
}

// 新增:信息提示工厂(无需修改原有工厂)
class InfoToastFactory extends ToastFactory {
  createToast(message) {
    return new InfoToast(message);
  }
}

// 5. 使用者调用(根据场景选择具体工厂)
function showToast(factory, message) {
  const toast = factory.createToast(message);
  toast.show();
}

// 使用成功提示工厂
showToast(new SuccessToastFactory(), '提交成功'); // ✅ 成功:提交成功
// 使用信息提示工厂(新增功能,原有代码无修改)
showToast(new InfoToastFactory(), '请完善资料'); // ℹ️ 信息:请完善资料

优势:新增提示类型时,只需新增InfoToast(产品)和InfoToastFactory(工厂),完全符合 “开闭原则”(对扩展开放,对修改关闭)。

  1. 抽象工厂模式(Abstract Factory)

当需要创建一系列相互关联或依赖的对象(如一套主题下的按钮、输入框、弹窗)时,抽象工厂模式可以保证这些对象的 “兼容性”。

核心思想:一个工厂负责创建一整套 “产品家族”,而不是单个产品。

前端示例:创建不同主题的 UI 组件(按钮 + 输入框)

假设需要支持 “浅色主题” 和 “深色主题”,每种主题包含配套的按钮(Button)和输入框(Input)。

// 1. 定义产品家族接口(按钮和输入框)
class Button {
  render() { throw new Error('子类必须实现render'); }
}

class Input {
  render() { throw new Error('子类必须实现render'); }
}

// 2. 具体产品:浅色主题组件
class LightButton extends Button {
  render() { return '<button style="background: white; color: black">浅色按钮</button>'; }
}

class LightInput extends Input {
  render() { return '<input style="background: white; border: 1px solid #ccc">'; }
}

// 3. 具体产品:深色主题组件
class DarkButton extends Button {
  render() { return '<button style="background: black; color: white">深色按钮</button>'; }
}

class DarkInput extends Input {
  render() { return '<input style="background: #333; color: white; border: none">'; }
}

// 4. 抽象工厂接口(定义创建产品家族的方法)
class UIFactory {
  createButton() { throw new Error('子类必须实现createButton'); }
  createInput() { throw new Error('子类必须实现createInput'); }
}

// 5. 具体工厂:浅色主题工厂(创建一整套浅色组件)
class LightUIFactory extends UIFactory {
  createButton() { return new LightButton(); }
  createInput() { return new LightInput(); }
}

// 6. 具体工厂:深色主题工厂(创建一整套深色组件)
class DarkUIFactory extends UIFactory {
  createButton() { return new DarkButton(); }
  createInput() { return new DarkInput(); }
}

// 7. 使用者调用(根据主题选择工厂,确保组件风格一致)
function renderUI(factory) {
  const button = factory.createButton();
  const input = factory.createInput();
  console.log('渲染UI:');
  console.log(button.render());
  console.log(input.render());
}

// 渲染浅色主题
renderUI(new LightUIFactory());
// 输出:
// <button style="background: white; color: black">浅色按钮</button>
// <input style="background: white; border: 1px solid #ccc">

// 渲染深色主题
renderUI(new DarkUIFactory());
// 输出:
// <button style="background: black; color: white">深色按钮</button>
// <input style="background: #333; color: white; border: none">

优势:确保同一主题下的组件风格统一(如浅色按钮 + 浅色输入框),避免出现 “深色按钮配浅色输入框” 的混乱组合。

总结

  • 简单工厂:适合简单场景,用一个函数根据参数创建对象(逻辑集中,扩展略繁琐)。
  • 工厂方法:适合需要频繁扩展的场景,每个产品对应一个工厂(符合开闭原则)。
  • 抽象工厂:适合创建 “产品家族”(如主题、套件),保证对象间的兼容性。

1.3单例模式

单例模式(Singleton)是一种非常简单且容易理解的设计模式。顾名思义,单例即单一的实例,确切地讲就是指在某个系统中只存在一个实例,同时提供集中、统一的访问接口,以使系统行为保持协调一致。singleton一词在逻辑学中指“有且仅有一个元素的集合”​,这非常恰当地概括了单例的概念,也就是“一个类仅有一个实例”​。

前端中,需要 “全局唯一” 的场景都适合用单例模式,例如:

  • 全局状态管理器(如 Redux 的store、Vuex 的store);
  • 工具类:如日期格式化工具、权限校验工具,全局只需一个实例即可满足需求。
  • 全局事件总线(EventBus,确保事件监听 / 触发的唯一性);
  • 浏览器环境的全局对象(如windowdocument,本质上是单例)。

前端示例:实现全局弹窗管理器(ModalManager)

假设应用中需要一个弹窗管理器,负责控制弹窗的显示 / 隐藏,且整个应用只能有一个管理器实例(避免多个管理器同时操作 DOM 导致冲突)

  1. 基础实现:ES6 类 + 静态实例

通过静态属性保存唯一实例,在构造函数中判断是否已存在实例,若存在则直接返回已有实例(或抛出错误)。

class ModalManager {
  // 静态属性:保存唯一实例
  static instance;

  // 构造函数:私有逻辑(禁止外部直接new)
  constructor() {
    // 关键:如果已有实例,直接返回已有实例(阻止创建新实例)
    if (ModalManager.instance) {
      return ModalManager.instance;
    }
    // 初始化逻辑(如绑定DOM、状态管理)
    this.modals = []; // 管理所有弹窗的数组
    this.isShowing = false; // 是否有弹窗正在显示
    // 保存实例到静态属性
    ModalManager.instance = this;
  }

  // 静态方法:提供全局访问点
  static getInstance() {
    if (!ModalManager.instance) {
      // 若实例不存在,创建它
      new ModalManager();
    }
    return ModalManager.instance;
  }

  // 实例方法:添加弹窗
  addModal(modal) {
    this.modals.push(modal);
    console.log(`已添加弹窗,当前共${this.modals.length}个`);
  }

  // 实例方法:显示弹窗(确保同一时间只显示一个)
  showModal() {
    if (this.isShowing) {
      console.log('已有弹窗正在显示,无法重复显示');
      return;
    }
    if (this.modals.length === 0) {
      console.log('没有可显示的弹窗');
      return;
    }
    this.isShowing = true;
    console.log('显示弹窗:', this.modals[0]);
  }

  // 实例方法:隐藏弹窗
  hideModal() {
    this.isShowing = false;
    console.log('隐藏弹窗');
  }
}
  1. 使用方式:全局唯一实例验证
// 尝试创建多个实例
const manager1 = new ModalManager();
const manager2 = new ModalManager();
const manager3 = ModalManager.getInstance();

// 验证是否为同一个实例
console.log(manager1 === manager2); // true(引用相同)
console.log(manager1 === manager3); // true(引用相同)

// 调用实例方法(所有引用操作的是同一个实例)
manager1.addModal({ id: 1, content: '弹窗1' }); // 已添加弹窗,当前共1个
manager2.addModal({ id: 2, content: '弹窗2' }); // 已添加弹窗,当前共2个
manager3.showModal(); // 显示弹窗:{ id: 1, content: '弹窗1' }
manager1.hideModal(); // 隐藏弹窗

关键点:无论通过new ModalManager()还是getInstance(),获取的都是同一个实例,确保了全局唯一性。

  1. 更简洁的实现:模块模式(利用 ES6 模块的单例特性)

ES6 模块本身就是单例的(模块在首次导入时执行,后续导入会直接复用已导出的内容),因此可以用模块模式更简单地实现单例:

// modalManager.js(ES6模块,天然单例)
let modals = [];
let isShowing = false;

// 模块内定义方法(操作的是模块内的私有变量)
const addModal = (modal) => {
  modals.push(modal);
  console.log(`已添加弹窗,当前共${modals.length}个`);
};

const showModal = () => {
  if (isShowing) {
    console.log('已有弹窗正在显示');
    return;
  }
  if (modals.length === 0) {
    console.log('没有可显示的弹窗');
    return;
  }
  isShowing = true;
  console.log('显示弹窗:', modals[0]);
};

const hideModal = () => {
  isShowing = false;
  console.log('隐藏弹窗');
};

// 导出方法(整个应用中,导入的都是这组方法,共享同一套变量)
export default { addModal, showModal, hideModal };

使用时,无论在多少个文件中导入,操作的都是同一个模块内的变量:

// page1.js
import modalManager from './modalManager.js';
modalManager.addModal({ id: 1 }); // 已添加弹窗,当前共1个

// page2.js
import modalManager from './modalManager.js';
modalManager.addModal({ id: 2 }); // 已添加弹窗,当前共2个
modalManager.showModal(); // 显示弹窗:{ id: 1 }

优势:利用 ES6 模块的天然单例特性,无需手动管理实例,代码更简洁,是前端开发中最常用的单例实现方式。

2.结构型

2.1 装饰器模式

装饰指在某物件上装点额外饰品的行为,以使其原本朴素的外表变得更加饱满、华丽,而装饰器(装饰者)就是能够化“腐朽”为神奇的利器。装饰器模式(Decorator)能够在运行时动态地为原始对象增加一些额外的功能,使其变得更加强大。从某种程度上讲,装饰器非常类似于“继承”​,它们都是为了增强原始对象的功能,区别在于方式的不同,后者是在编译时(compile-time)静态地通过对原始类的继承完成,而前者则是在程序运行时(run-time)通过对原始对象动态地“包装”完成,是对类实例(对象)​“装饰”的结果。

核心角色

  1. 抽象组件(Component) :定义被装饰对象和装饰器的共同接口(如方法),确保装饰器可以替代被装饰对象。
  2. 具体组件(Concrete Component) :需要被装饰的原始对象(如基础按钮、基础输入框)。
  3. 抽象装饰器(Decorator) :实现抽象组件接口,内部持有一个组件实例(被装饰的对象),并定义装饰器的基础结构。
  4. 具体装饰器(Concrete Decorator) :继承抽象装饰器,负责给组件添加具体的额外功能(如给按钮加图标、加日志)。

前端开发中,装饰器模式常用于动态扩展组件 / 函数的功能,典型场景包括:

  • 给 UI 组件添加额外功能(如按钮加图标、加权限控制、加点击日志);
  • 给函数添加横切逻辑(如防抖、节流、日志打印、错误捕获);
  • React中的高阶组件(HOC)、React Hooks 的组合(如useEffect包装组件生命周期)

前端示例 1:装饰器模式扩展按钮功能

假设需要实现一个基础按钮,然后动态给它添加 “图标”“点击日志”“权限控制” 等功能,且这些功能可以灵活组合。

  1. 定义抽象组件和具体组件(基础按钮)
// 1. 抽象组件:定义按钮的核心接口(render和onClick)
class Button {
  constructor(label) {
    this.label = label; // 按钮文本
  }

  // 渲染按钮(返回HTML字符串)
  render() {
    return `<button>${this.label}</button>`;
  }

  // 点击事件处理
  onClick() {
    console.log(`按钮【${this.label}】被点击`);
  }
}

// 2. 具体组件:基础按钮(被装饰的原始对象)
class BasicButton extends Button {
  // 继承父类的render和onClick,无需修改
}
  1. 定义抽象装饰器和具体装饰器(扩展功能)
// 3. 抽象装饰器:实现Button接口,持有被装饰的按钮实例
class ButtonDecorator extends Button {
  constructor(button) {
    super(button.label); // 复用原始按钮的label
    this.button = button; // 持有被装饰的按钮实例
  }

  // 重写render:默认调用被装饰对象的render(具体装饰器可扩展)
  render() {
    return this.button.render();
  }

  // 重写onClick:默认调用被装饰对象的onClick(具体装饰器可扩展)
  onClick() {
    this.button.onClick();
  }
}

// 4. 具体装饰器1:给按钮添加图标
class WithIcon extends ButtonDecorator {
  constructor(button, icon) {
    super(button);
    this.icon = icon; // 图标(如"🔍")
  }

  // 扩展render:在按钮文本前添加图标
  render() {
    const originalHtml = super.render();
    // 替换按钮文本为“图标+文本”
    return originalHtml.replace(`>${this.label}</`, `>${this.icon} ${this.label}</`);
  }
}

// 5. 具体装饰器2:给按钮添加点击日志(记录点击时间)
class WithClickLog extends ButtonDecorator {
  // 扩展onClick:在原始点击逻辑前添加日志
  onClick() {
    const time = new Date().toLocaleTimeString();
    console.log(`【日志】按钮【${this.label}】在${time}被点击`);
    super.onClick(); // 调用原始点击逻辑
  }
}

// 6. 具体装饰器3:给按钮添加权限控制(无权限时禁用)
class WithPermission extends ButtonDecorator {
  constructor(button, hasPermission) {
    super(button);
    this.hasPermission = hasPermission; // 是否有权限
  }

  // 扩展render:无权限时添加disabled属性
  render() {
    let originalHtml = super.render();
    if (!this.hasPermission) {
      originalHtml = originalHtml.replace('<button', '<button disabled style="opacity: 0.5"');
    }
    return originalHtml;
  }

  // 扩展onClick:无权限时阻止点击
  onClick() {
    if (!this.hasPermission) {
      console.log(`【权限】无权限操作按钮【${this.label}】`);
      return; // 不执行原始点击逻辑
    }
    super.onClick();
  }
}
  1. 组合装饰器:动态扩展功能
// 创建基础按钮
const basicBtn = new BasicButton('提交');
console.log('基础按钮渲染:', basicBtn.render()); 
// 输出:<button>提交</button>
basicBtn.onClick(); 
// 输出:按钮【提交】被点击


// 装饰1:给按钮添加图标
const iconBtn = new WithIcon(basicBtn, '🚀');
console.log('带图标按钮渲染:', iconBtn.render()); 
// 输出:<button>🚀 提交</button>
iconBtn.onClick(); 
// 输出:按钮【提交】被点击(保留原始点击逻辑)


// 装饰2:同时添加图标+点击日志
const iconLogBtn = new WithClickLog(iconBtn);
console.log('带图标+日志按钮渲染:', iconLogBtn.render()); 
// 输出:<button>🚀 提交</button>(图标保留)
iconLogBtn.onClick(); 
// 输出:【日志】按钮【提交】在15:30:20被点击  
// 输出:按钮【提交】被点击(原始逻辑)


// 装饰3:同时添加图标+日志+权限控制(有权限)
const permissionBtn = new WithPermission(iconLogBtn, true); // hasPermission为true
console.log('有权限按钮渲染:', permissionBtn.render()); 
// 输出:<button>🚀 提交</button>(不禁用)
permissionBtn.onClick(); 
// 输出:【日志】按钮【提交】在15:30:25被点击  
// 输出:按钮【提交】被点击


// 装饰4:无权限场景
const noPermissionBtn = new WithPermission(iconLogBtn, false); // hasPermission为false
console.log('无权限按钮渲染:', noPermissionBtn.render()); 
// 输出:<button disabled style="opacity: 0.5">🚀 提交</button>(禁用)
noPermissionBtn.onClick(); 
// 输出:【权限】无权限操作按钮【提交】(不执行原始逻辑)

前端示例 2:ES7 装饰器语法(函数装饰)

JavaScript(ES7)有装饰器提案(目前处于 Stage 3),可通过@装饰器名语法简化装饰器模式的使用,常用于给类或方法添加功能。

以 “给函数添加防抖功能” 为例:

// 定义防抖装饰器(具体装饰器)
function debounce(delay) {
  return function (target, name, descriptor) {
    const originalMethod = descriptor.value; // 原始函数
    let timer;
    // 包装原始函数:添加防抖逻辑
    descriptor.value = function (...args) {
      clearTimeout(timer);
      timer = setTimeout(() => {
        originalMethod.apply(this, args); // 延迟执行原始函数
      }, delay);
    };
    return descriptor;
  };
}

// 被装饰的类(具体组件)
class SearchBox {
  // 用@debounce装饰search方法,添加防抖功能(延迟500ms)
  @debounce(500)
  search(keyword) {
    console.log(`搜索:${keyword}`);
  }
}

// 使用
const searchBox = new SearchBox();
searchBox.search('a'); // 触发后500ms内再次调用会重置定时器
searchBox.search('ab'); // 500ms后仅执行最后一次调用
// 输出:搜索:ab(500ms后)

前端示例 3:React 高阶组件(HOC)—— 装饰器模式的典型应用

React 中的高阶组件(HOC)本质是装饰器模式的实现:接收一个组件,返回一个添加了额外功能的新组件。

// 定义HOC(装饰器):给组件添加加载状态
function withLoading(WrappedComponent) {
  // 返回新组件(装饰后的组件)
  return function (props) {
    if (props.isLoading) {
      return <div>加载中...</div>; // 加载状态
    }
    return <WrappedComponent {...props} />; // 渲染原始组件
  };
}

// 原始组件(具体组件)
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 用HOC装饰原始组件,添加加载功能
const UserListWithLoading = withLoading(UserList);

// 使用装饰后的组件
function App() {
  const [isLoading, setIsLoading] = React.useState(true);
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    // 模拟请求
    setTimeout(() => {
      setUsers([{ id: 1, name: '张三' }, { id: 2, name: '李四' }]);
      setIsLoading(false);
    }, 1000);
  }, []);

  return <UserListWithLoading isLoading={isLoading} users={users} />;
}

这里withLoading是装饰器,UserList是被装饰的组件,装饰后得到UserListWithLoading,新增了 “加载状态” 功能,且未修改UserList的源码。

2.2 适配器模式

适配器模式(Adapter)通常也被称为转换器,顾名思义,它一定是进行适应与匹配工作的物件。当一个对象或类的接口不能匹配用户所期待的接口时,适配器就充当中间转换的角色,以达到兼容用户接口的目的,同时适配器也实现了客户端与接口的解耦,提高了组件的可复用性。

对象是多样化的,对象之间通过信息交换,也就是互动、沟通,世界才充满生机,否则就是死水一潭。人类最常用的沟通方式就是语言,两个人对话时,一方通过嘴巴发出声音,另一方则通过耳朵接收这些语言信息,所以嘴巴和耳朵(接口)必须兼容同一种语言(参数)才能达到沟通的目的。试想,我们跟不懂中文的人讲中文一定是徒劳的,因为对方根本无法理解我们在讲什么,更不要说人类和动物对话了,接口不兼容的结果就是对牛弹琴。

要跨越语言的鸿沟就必须找个会两种语言的翻译,将接口转换才能使沟通进行下去,我们将翻译这个角色称为适配器。适配器在我们生活中非常常见,比如手机充电口是 Type-C,而插座是 USB-A,这时转接头(适配器)就能让它们兼容。在代码中,适配器用于解决 “新旧接口不匹配”“第三方库接口与本地需求不一致” 等问题。

核心角色

  1. 目标接口(Target) :客户端期望的接口格式(如本地系统定义的方法)。
  2. 适配者(Adaptee) :需要被适配的原有接口(如旧系统、第三方库的接口,格式与目标接口不兼容)。
  3. 适配器(Adapter) :实现目标接口,并内部包装适配者,将适配者的接口转换为目标接口格式。

前端示例 1:数据格式适配器(新旧接口兼容)

假设项目中原本使用旧接口获取用户数据,返回格式为:

// 旧接口返回(适配者)
{
  user_name: "张三",
  user_age: 25,
  user_email: "zhangsan@example.com"
}

新系统要求用户数据格式为:

// 目标接口格式(客户端期望)
{
  name: "张三",
  age: 25,
  email: "zhangsan@example.com"
}

此时可以用适配器转换格式,让新系统无需修改即可兼容旧接口。

// 1. 适配者(Adaptee):旧接口的数据格式(需要被适配)
class OldUserService {
  // 模拟调用旧接口获取数据
  fetchUser() {
    return {
      user_name: "张三",
      user_age: 25,
      user_email: "zhangsan@example.com"
    };
  }
}

// 2. 目标接口(Target):新系统期望的数据格式接口
class UserService {
  // 新系统期望的方法:返回 { name, age, email }
  getUser() {
    throw new Error("子类需实现getUser方法");
  }
}

// 3. 适配器(Adapter):将旧接口转换为新接口
class UserAdapter extends UserService {
  constructor() {
    super();
    this.oldService = new OldUserService(); // 包装适配者
  }

  // 实现目标接口的方法,内部调用旧接口并转换格式
  getUser() {
    const oldData = this.oldService.fetchUser(); // 调用旧接口
    // 转换格式:将user_name → name,user_age → age,user_email → email
    return {
      name: oldData.user_name,
      age: oldData.user_age,
      email: oldData.user_email
    };
  }
}

// 4. 客户端使用(只依赖目标接口,无需关心适配细节)
function displayUser(userService) {
  const user = userService.getUser();
  console.log(`姓名:${user.name},年龄:${user.age},邮箱:${user.email}`);
}

// 用适配器适配旧接口
const adapter = new UserAdapter();
displayUser(adapter); // 输出:姓名:张三,年龄:25,邮箱:zhangsan@example.com

关键点:客户端(displayUser)只依赖目标接口(UserService),适配器(UserAdapter)隐藏了旧接口的细节,实现了无缝兼容。

前端示例 2:第三方库适配器(API 风格统一)

假设项目中使用第三方日期库OldDateLib,其格式化日期的方法是formatDate(timestamp, formatStr),而本地代码习惯使用format(timestamp, format)(参数名更简洁)。用适配器可以统一 API 风格。

// 1. 适配者(Adaptee):第三方库的接口(参数名与本地不一致)
const OldDateLib = {
  // 第三方库的方法:参数名为timestamp和formatStr
  formatDate: function(timestamp, formatStr) {
    const date = new Date(timestamp);
    // 简化的格式化逻辑(仅示例)
    return formatStr.replace('YYYY', date.getFullYear())
                    .replace('MM', date.getMonth() + 1)
                    .replace('DD', date.getDate());
  }
};

// 2. 目标接口(Target):本地期望的API风格(参数名为timestamp和format)
class DateFormatter {
  format(timestamp, format) {
    throw new Error("需实现format方法");
  }
}

// 3. 适配器(Adapter):包装第三方库,统一参数名
class DateLibAdapter extends DateFormatter {
  format(timestamp, format) {
    // 调用第三方库的方法,但将本地参数名(format)映射为第三方需要的参数名(formatStr)
    return OldDateLib.formatDate(timestamp, format);
  }
}

// 4. 客户端使用(按本地习惯调用,无需关心第三方库的参数细节)
const formatter = new DateLibAdapter();
// 本地调用时用format参数名,适配器会转换为第三方库的formatStr
console.log(formatter.format(1620000000000, 'YYYY-MM-DD')); // 输出:2021-5-3

优势:如果未来替换第三方库(如换成moment.js),只需修改适配器的实现,无需改动所有调用format的客户端代码。

2.3 代理模式

代理模式(Proxy)​,顾名思义,有代表打理的意思。某些情况下,当客户端不能或不适合直接访问目标业务对象时,业务对象可以通过代理把自己的业务托管起来,使客户端间接地通过代理进行业务访问。如此不但能方便用户使用,还能对客户端的访问进行一定的控制。简单来说,就是代理方以业务对象的名义,代理了它的业务。

在我们的社会活动中存在着各种各样的代理,例如销售代理商,他们受商品制造商委托负责代理商品的销售业务,而购买方(如最终消费者)则不必与制造商发生关联,也不用关心商品的具体制造过程,而是直接找代理商购买产品。

顾客通常不会找汽车制造商直接购买汽车,而是通过4S店购买。介于顾客与制造商之间,4S店对汽车制造商生产的整车与零配件提供销售代理服务,并且在制造商原本职能的基础之上增加了一些额外的附加服务,如汽车上牌、注册、保养、维修等,使顾客与汽车制造商彻底脱离关系。除此之外,代理模式的示例还有明星经纪人对明星推广业务的代理;律师对原告或被告官司的代理;旅游团对门票、机票业务的代理等。

核心角色

  1. 抽象主题(Subject) :定义目标对象和代理对象的共同接口(如方法),确保代理可替代目标对象。
  2. 真实主题(Real Subject) :被代理的目标对象,包含核心业务逻辑。
  3. 代理(Proxy) :实现抽象主题接口,内部持有真实主题的引用,控制对真实主题的访问,并可添加额外逻辑。

前端开发中,代理模式常用于拦截操作、增强功能,典型场景包括:

  • 权限控制(如未登录用户禁止访问某些功能);
  • 数据缓存(如缓存 API 请求结果,避免重复请求);
  • 延迟加载(如图片懒加载,代理先显示占位图,再加载真实图片);
  • 日志记录(如记录函数调用参数和返回值);
  • 防抖 / 节流(代理拦截频繁触发的事件,控制执行频率)。

前端示例 1:权限代理(控制对敏感操作的访问)

假设系统中有一个 “删除数据” 的功能,需要验证用户是否有权限(如管理员),否则禁止操作。用代理模式可以在调用真实删除逻辑前拦截并校验权限。

// 1. 抽象主题:定义删除功能的接口
class DataDeleter {
  delete(id) {
    throw new Error("子类需实现delete方法");
  }
}

// 2. 真实主题:实际执行删除操作的对象(核心逻辑)
class RealDataDeleter extends DataDeleter {
  delete(id) {
    console.log(`[真实操作] 删除ID为${id}的数据`);
    // 实际删除逻辑(如调用API)
  }
}

// 3. 代理:权限代理,控制对真实删除功能的访问
class PermissionProxy extends DataDeleter {
  constructor(realDeleter, userRole) {
    super();
    this.realDeleter = realDeleter; // 持有真实主题的引用
    this.userRole = userRole; // 用户角色(如'admin'或'user')
  }

  // 重写delete方法:先校验权限,再决定是否调用真实删除
  delete(id) {
    if (this.userRole === 'admin') {
      // 有权限:调用真实删除逻辑
      this.realDeleter.delete(id);
    } else {
      // 无权限:拦截操作并提示
      console.log(`[权限拦截] 角色${this.userRole}无删除权限`);
    }
  }
}

// 4. 客户端使用(通过代理访问,无需直接操作真实对象)
// 创建真实删除器
const realDeleter = new RealDataDeleter();

// 管理员用户:通过代理操作(有权限)
const adminProxy = new PermissionProxy(realDeleter, 'admin');
adminProxy.delete(100); // 输出:[真实操作] 删除ID为100的数据

// 普通用户:通过代理操作(无权限)
const userProxy = new PermissionProxy(realDeleter, 'user');
userProxy.delete(100); // 输出:[权限拦截] 角色user无删除权限

前端示例 2:缓存代理(优化 API 请求性能)

对于频繁调用的 API(如获取用户信息),可以用代理模式缓存结果,避免重复请求服务器。

// 1. 真实主题:实际发起API请求的函数
class UserApi {
  // 模拟异步请求用户信息(返回Promise)
  fetchUser(id) {
    console.log(`[API请求] 正在获取用户${id}的数据...`);
    return new Promise((resolve) => {
      setTimeout(() => {
        // 模拟服务器返回数据
        resolve({ id, name: `用户${id}`, age: 20 + id });
      }, 1000);
    });
  }
}

// 2. 代理:缓存代理,缓存请求结果
class CacheProxy {
  constructor(api) {
    this.api = api; // 持有真实API对象
    this.cache = new Map(); // 缓存容器(key: 用户ID,value: 数据)
  }

  // 重写fetchUser:先查缓存,无缓存再请求
  async fetchUser(id) {
    // 1. 检查缓存
    if (this.cache.has(id)) {
      console.log(`[缓存命中] 直接返回用户${id}的缓存数据`);
      return this.cache.get(id);
    }

    // 2. 无缓存:调用真实API请求
    const data = await this.api.fetchUser(id);

    // 3. 缓存结果
    this.cache.set(id, data);
    console.log(`[缓存更新] 已缓存用户${id}的数据`);

    return data;
  }
}

// 3. 客户端使用
async function test() {
  const api = new UserApi();
  const proxy = new CacheProxy(api);

  // 第一次请求(无缓存)
  const user1 = await proxy.fetchUser(1);
  console.log('结果:', user1); 
  // 输出:[API请求] 正在获取用户1的数据... → [缓存更新] 已缓存用户1的数据 → 结果:{id:1, ...}

  // 第二次请求(有缓存)
  const user1Again = await proxy.fetchUser(1);
  console.log('结果:', user1Again); 
  // 输出:[缓存命中] 直接返回用户1的缓存数据 → 结果:{id:1, ...}(无需等待1秒)
}

test();

前端示例 3:ES6 原生 Proxy(对象代理)

JavaScript ES6 提供了原生的 Proxy 对象,可直接创建代理,拦截对象的读取、修改、函数调用等操作,是实现代理模式的便捷工具。

示例:用 ES6 Proxy 实现日志代理(记录对象操作)

// 目标对象(真实主题):一个用户信息对象
const user = {
  name: '张三',
  age: 25,
  updateAge(newAge) {
    this.age = newAge;
  }
};

// 创建代理(Proxy):拦截对象操作并记录日志
const userProxy = new Proxy(user, {
  // 拦截属性读取(如 userProxy.name)
  get(target, prop) {
    console.log(`[日志] 读取属性${prop},当前值:${target[prop]}`);
    return target[prop];
  },

  // 拦截属性修改(如 userProxy.age = 26)
  set(target, prop, value) {
    console.log(`[日志] 修改属性${prop},旧值:${target[prop]},新值:${value}`);
    target[prop] = value;
    return true;
  },

  // 拦截函数调用(如 userProxy.updateAge(26))
  apply(target, thisArg, args) {
    console.log(`[日志] 调用方法${target.name},参数:${args}`);
    return target.apply(thisArg, args);
  }
});

// 访问代理(间接操作目标对象)
userProxy.name; // 输出:[日志] 读取属性name,当前值:张三
userProxy.age = 26; // 输出:[日志] 修改属性age,旧值:25,新值:26
userProxy.updateAge(27); // 输出:[日志] 调用方法updateAge,参数:27
console.log(user.age); // 输出:27(目标对象已被修改)

优势:ES6 Proxy 支持拦截多种操作(如 getsetapplydeleteProperty 等),无需手动实现抽象主题接口,更适合前端灵活的对象代理场景。

前端示例 4:图片懒加载(代理模式的经典应用)

图片懒加载的核心是:先显示占位图,当图片进入视口时再加载真实图片。这里的 “占位图逻辑” 就是代理,负责控制真实图片的加载时机。

<!-- 页面中的图片标签,先用data-src存储真实地址,src指向占位图 -->
<img class="lazy-img" data-src="real-image.jpg" src="placeholder.png" alt="图片">
// 代理逻辑:控制图片加载时机
class LazyImageProxy {
  constructor(imgElement) {
    this.imgElement = imgElement; // 图片DOM元素
    this.realSrc = imgElement.dataset.src; // 真实图片地址
    this.init();
  }

  // 初始化:监听滚动事件,判断是否进入视口
  init() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // 进入视口:加载真实图片(调用真实主题的逻辑)
          this.loadRealImage();
          observer.unobserve(this.imgElement); // 只监听一次
        }
      });
    });
    observer.observe(this.imgElement);
  }

  // 加载真实图片(真实主题的逻辑)
  loadRealImage() {
    const img = new Image();
    img.src = this.realSrc;
    img.onload = () => {
      this.imgElement.src = this.realSrc; // 替换为真实图片
      console.log(`[图片加载] ${this.realSrc} 已加载`);
    };
  }
}

// 初始化所有懒加载图片
document.querySelectorAll('.lazy-img').forEach(img => {
  new LazyImageProxy(img); // 每个图片都由代理控制
});

效果:页面滚动时,只有当图片进入视口,代理才会触发真实图片的加载,减少初始加载资源,优化性能。

3.行为型

3.1 发布订阅模式

发布订阅模式(Publish-Subscribe Pattern,简称 Pub/Sub)是一种行为型设计模式,核心思想是:引入一个 “事件中心”(中间件),发布者(Publisher)和订阅者(Subscriber)通过事件中心间接通信,两者完全解耦,互不感知对方的存在

简单说,就是 “发布者只管发布事件,订阅者只管订阅事件,事件中心负责转发通知”—— 就像报纸订阅:读者(订阅者)向报社(事件中心)订阅报纸,报社收到出版社(发布者)的报纸后,统一分发给所有订阅者,读者和出版社无需直接联系。

核心角色

  1. 发布者(Publisher) :无需关心订阅者是谁,只需向事件中心发布特定主题的事件(携带数据)。

  2. 订阅者(Subscriber) :无需关心发布者是谁,只需向事件中心订阅感兴趣的事件主题,收到通知后执行自身逻辑。

  3. 事件中心(Event Bus/Event Hub) :核心中间件,负责:

    • 维护 “事件主题 → 订阅者列表” 的映射关系;
    • 接收发布者的事件并转发给对应订阅者;
    • 提供订阅(on)、取消订阅(off)、发布(emit)的 API。

前端示例 1:通用全局事件总线(EventBus)

这是前端最常用的发布订阅模式实现,可直接用于跨组件通信,支持多事件主题、订阅 / 取消订阅、一次性订阅等功能。

class EventBus {
  constructor() {
    // 存储事件映射:key=事件主题(字符串),value=订阅者回调数组
    this.eventMap = new Map();
  }

  /**
   * 订阅事件
   * @param {string} topic - 事件主题
   * @param {Function} callback - 订阅回调(接收发布者传递的参数)
   * @returns {void}
   */
  on(topic, callback) {
    if (typeof callback !== 'function') {
      throw new Error('订阅回调必须是函数');
    }
    // 若主题不存在,初始化回调数组
    if (!this.eventMap.has(topic)) {
      this.eventMap.set(topic, []);
    }
    // 添加回调到主题对应的数组
    this.eventMap.get(topic).push(callback);
    console.log(`[EventBus] 订阅主题 "${topic}",当前订阅数:${this.eventMap.get(topic).length}`);
  }

  /**
   * 取消订阅
   * @param {string} topic - 事件主题
   * @param {Function} callback - 要取消的订阅回调(必须是之前订阅时的同一个函数引用)
   * @returns {void}
   */
  off(topic, callback) {
    if (!this.eventMap.has(topic)) return;
    const callbacks = this.eventMap.get(topic);
    // 过滤掉要取消的回调
    const newCallbacks = callbacks.filter(cb => cb !== callback);
    this.eventMap.set(topic, newCallbacks);
    console.log(`[EventBus] 取消订阅主题 "${topic}",当前订阅数:${newCallbacks.length}`);
    // 若主题无订阅者,移除该主题(优化内存)
    if (newCallbacks.length === 0) {
      this.eventMap.delete(topic);
      console.log(`[EventBus] 主题 "${topic}" 已无订阅者,移除该主题`);
    }
  }

  /**
   * 发布事件
   * @param {string} topic - 事件主题
   * @param  {...any} args - 传递给订阅者的参数(可多个)
   * @returns {void}
   */
  emit(topic, ...args) {
    if (!this.eventMap.has(topic)) {
      console.log(`[EventBus] 主题 "${topic}" 无订阅者,发布失败`);
      return;
    }
    const callbacks = this.eventMap.get(topic);
    console.log(`[EventBus] 发布主题 "${topic}",触发 ${callbacks.length} 个订阅者,参数:${args}`);
    // 遍历所有订阅回调,传递参数并执行
    callbacks.forEach(callback => {
      try {
        callback(...args); // 订阅者接收参数
      } catch (err) {
        console.error(`[EventBus] 订阅者回调执行失败:`, err);
      }
    });
  }

  /**
   * 一次性订阅(触发一次后自动取消订阅)
   * @param {string} topic - 事件主题
   * @param {Function} callback - 订阅回调
   * @returns {void}
   */
  once(topic, callback) {
    // 包装回调:执行后自动取消订阅
    const wrapCallback = (...args) => {
      callback(...args);
      this.off(topic, wrapCallback); // 触发后立即取消
    };
    this.on(topic, wrapCallback);
  }
}

// 导出全局单例(确保整个应用使用同一个事件中心)
export const globalEventBus = new EventBus();

应用示例(React 跨组件通信)

假设有 3 个无关联组件:LoginForm(登录表单,发布登录事件)、UserInfo(用户信息,订阅登录事件更新信息)、Notification(通知组件,订阅登录事件显示提示)。

实现效果:点击 “登录” 按钮后,LoginForm 发布 user-login 事件,UserInfo 和 Notification 同时收到通知:

  • UserInfo 更新用户信息显示;
  • Notification 显示 3 秒登录欢迎提示;
  • 三个组件无直接依赖,完全解耦。
// 1. LoginForm.jsx(发布者)
import { globalEventBus } from './EventBus';

function LoginForm() {
  const handleLogin = () => {
    // 模拟登录请求成功
    const userData = { id: 1, name: '张三', role: 'admin' };
    console.log('登录成功,发布登录事件');
    // 发布主题为 "user-login" 的事件,携带用户数据
    globalEventBus.emit('user-login', userData);
  };

  return <button onClick={handleLogin}>登录</button>;
}

// 2. UserInfo.jsx(订阅者)
import { useEffect, useState } from 'react';
import { globalEventBus } from './EventBus';

function UserInfo() {
  const [user, setUser] = useState(null);

  // 订阅登录事件的回调
  const handleUserLogin = (userData) => {
    console.log('UserInfo 收到登录通知,更新用户信息');
    setUser(userData);
  };

  // 组件挂载时订阅,卸载时取消订阅(避免内存泄漏)
  useEffect(() => {
    globalEventBus.on('user-login', handleUserLogin);
    // 清理函数:组件卸载时取消订阅
    return () => {
      globalEventBus.off('user-login', handleUserLogin);
    };
  }, []);

  return (
    <div>
      <h3>用户信息</h3>
      {user ? (
        <div>姓名:{user.name},角色:{user.role}</div>
      ) : (
        <div>未登录</div>
      )}
    </div>
  );
}

// 3. Notification.jsx(订阅者)
import { useEffect, useState } from 'react';
import { globalEventBus } from './EventBus';

function Notification() {
  const [message, setMessage] = useState('');

  // 订阅登录事件的回调
  const handleUserLogin = (userData) => {
    console.log('Notification 收到登录通知,显示提示');
    setMessage(`欢迎 ${userData.name} 登录!`);
    // 3秒后隐藏提示
    setTimeout(() => setMessage(''), 3000);
  };

  useEffect(() => {
    globalEventBus.on('user-login', handleUserLogin);
    return () => {
      globalEventBus.off('user-login', handleUserLogin);
    };
  }, []);

  if (!message) return null;
  return <div className="notification">{message}</div>;
}

// 4. 应用入口 App.jsx
function App() {
  return (
    <div>
      <LoginForm />
      <UserInfo />
      <Notification />
    </div>
  );
}

3.2 策略模式

策略,古时也称“计”​,指为了达成某个目标而提前策划好的方案。但计划往往不如变化快,当目标突变或者周遭情况不允许实施某方案的时候,我们就得临时变更方案。策略模式(Strategy)强调的是行为的灵活切换,比如一个类的多个方法有着类似的行为接口,可以将它们抽离出来作为一系列策略类,在运行时灵活对接,变更其算法策略,以适应不同的场景。

例如我们经常在电影中看到,特工在执行任务时总要准备好几套方案以应对突如其来的变化。实施过程中由于情况突变而导致预案无法继续实施A计划时,马上更换为B计划,以另一种行为方式达成目标。所以说提前策划非常重要,而随机应变的能力更是不可或缺,系统需要时刻确保灵活性、机动性才能立于不败之地。

核心角色

  1. 上下文(Context)

    • 持有当前使用的策略实例,提供统一的接口供客户端调用;
    • 负责接收客户端请求,并委托给当前策略执行具体逻辑(不关心策略的内部实现)。
  2. 策略接口(Strategy)

    • 定义所有策略必须实现的统一方法(如 executevalidate),确保策略的可替换性。
  3. 具体策略(Concrete Strategy)

    • 实现策略接口,封装具体的算法 / 行为(如 “手机号验证策略”“支付宝支付策略”)。

前端开发中,策略模式常用于处理多选项、多规则、多行为的场景,典型包括:

  • 表单验证(不同字段的验证规则:必填、手机号、邮箱、最小长度等);
  • 支付方式选择(支付宝、微信支付、银行卡支付,不同支付逻辑);
  • 数据排序 / 筛选(按时间排序、按价格排序、按热度筛选);
  • UI 主题切换(浅色、深色、自定义主题,不同样式策略);
  • 动画效果选择(淡入、滑动、缩放,不同动画逻辑)。

前端示例 1:表单验证(经典场景)

假设需要实现一个注册表单验证,包含以下规则:

  • 用户名:必填,最小长度 3;
  • 手机号:必填,格式正确;
  • 邮箱:可选,格式正确(若填写)。

传统写法会用大量 if-else,用策略模式可优雅解耦。

// 传统写法:用if-else判断不同验证规则
function validateForm(field, value) {
  if (field === 'username') {
    if (!value) return '用户名不能为空';
    if (value.length < 3) return '用户名至少3个字符';
    return '';
  } else if (field === 'phone') {
    if (!value) return '手机号不能为空';
    if (!/^1[3-9]\d{9}$/.test(value)) return '手机号格式错误';
    return '';
  } else if (field === 'email') {
    if (!value) return ''; // 可选字段
    if (!/^[\w-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9]+$/.test(value)) return '邮箱格式错误';
    return '';
  }
}

// 使用
console.log(validateForm('username', 'zh')); // 用户名至少3个字符
console.log(validateForm('phone', '123456')); // 手机号格式错误

弊端:新增字段(如密码强度验证)需修改 validateForm 函数,增加 else if,违反开闭原则;逻辑臃肿,难以维护。

策略模式写法(解耦、易扩展)

步骤 1:定义具体策略(验证规则)

// 具体策略:封装不同的验证规则(实现统一的 validate 方法)
const ValidationStrategies = {
  // 必填验证
  required: (value) => {
    return !value ? '此字段不能为空' : '';
  },
  // 最小长度验证
  minLength: (value, min) => {
    return value.length >= min ? '' : `至少${min}个字符`;
  },
  // 手机号格式验证
  phone: (value) => {
    return /^1[3-9]\d{9}$/.test(value) ? '' : '手机号格式错误';
  },
  // 邮箱格式验证
  email: (value) => {
    return /^[\w-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9]+$/.test(value) ? '' : '邮箱格式错误';
  }
};

步骤 2:定义上下文(表单验证器)

// 上下文:管理策略,提供统一的验证接口
class FormValidator {
  constructor() {
    this.rules = {}; // 存储字段的验证规则映射:{ 字段名: [策略1, 策略2] }
  }

  // 为字段添加验证规则(注册策略)
  addRule(field, strategies) {
    this.rules[field] = strategies;
  }

  // 执行验证(委托策略执行)
  validate(field, value) {
    const strategies = this.rules[field];
    if (!strategies || strategies.length === 0) return ''; // 无规则则通过

    // 遍历该字段的所有策略,执行验证
    for (const strategy of strategies) {
      const [strategyName, ...params] = strategy; // 解析策略名和参数(如 ['minLength', 3])
      const validateFn = ValidationStrategies[strategyName];
      if (!validateFn) throw new Error(`不存在策略:${strategyName}`);

      const errorMsg = validateFn(value, ...params);
      if (errorMsg) return errorMsg; // 有错误直接返回
    }
    return ''; // 所有策略通过
  }
}

步骤 3:客户端使用

// 1. 创建验证器实例(上下文)
const validator = new FormValidator();

// 2. 为字段注册验证规则(绑定策略)
validator.addRule('username', [
  ['required'], // 策略1:必填
  ['minLength', 3] // 策略2:最小长度3,参数3
]);

validator.addRule('phone', [
  ['required'], // 策略1:必填
  ['phone'] // 策略2:手机号格式
]);

validator.addRule('email', [
  ['email'] // 策略1:邮箱格式(可选字段,无required策略)
]);

// 3. 执行验证
console.log(validator.validate('username', 'zh')); // 至少3个字符
console.log(validator.validate('phone', '123456')); // 手机号格式错误
console.log(validator.validate('email', 'test@')); // 邮箱格式错误
console.log(validator.validate('username', 'zhang')); // ''(通过)
扩展:新增密码强度验证

无需修改原有代码,只需新增策略并注册:

// 新增策略:密码强度验证(包含字母+数字)
ValidationStrategies.passwordStrength = (value) => {
  return /^(?=.*[a-zA-Z])(?=.*\d).+$/.test(value) ? '' : '密码需包含字母和数字';
};

// 为password字段注册规则
validator.addRule('password', [
  ['required'],
  ['minLength', 6],
  ['passwordStrength'] // 新增策略
]);

console.log(validator.validate('password', '123456')); // 密码需包含字母和数字

前端示例 2:React 中的策略模式(UI 渲染策略)

在 React 中,可通过策略模式动态渲染不同 UI 组件(如不同状态的按钮、不同类型的表单字段)。

// 1. 具体策略:不同类型的表单字段组件
const FormFieldStrategies = {
  // 输入框
  input: ({ label, value, onChange }) => (
    <div className="form-field">
      <label>{label}</label>
      <input type="text" value={value} onChange={(e) => onChange(e.target.value)} />
    </div>
  ),
  // 下拉选择框
  select: ({ label, options, value, onChange }) => (
    <div className="form-field">
      <label>{label}</label>
      <select value={value} onChange={(e) => onChange(e.target.value)}>
        {options.map((opt) => (
          <option key={opt.value} value={opt.value}>{opt.label}</option>
        ))}
      </select>
    </div>
  ),
  // 复选框
  checkbox: ({ label, checked, onChange }) => (
    <div className="form-field">
      <label>
        <input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
        {label}
      </label>
    </div>
  )
};

// 2. 上下文:表单字段渲染器(可复用组件)
function FormField({ type, ...props }) {
  const FieldComponent = FormFieldStrategies[type];
  if (!FieldComponent) throw new Error(`不支持的字段类型:${type}`);
  // 委托策略组件渲染
  return <FieldComponent {...props} />;
}

// 3. 客户端使用(表单组件)
function UserForm() {
  const [formData, setFormData] = React.useState({
    name: '',
    gender: 'male',
    agree: false
  });

  const handleChange = (key, value) => {
    setFormData({ ...formData, [key]: value });
  };

  return (
    <form className="user-form">
      {/* 输入框策略 */}
      <FormField
        type="input"
        label="姓名"
        value={formData.name}
        onChange={(value) => handleChange('name', value)}
      />
      {/* 下拉选择框策略 */}
      <FormField
        type="select"
        label="性别"
        options={[
          { value: 'male', label: '' },
          { value: 'female', label: '' }
        ]}
        value={formData.gender}
        onChange={(value) => handleChange('gender', value)}
      />
      {/* 复选框策略 */}
      <FormField
        type="checkbox"
        label="同意协议"
        checked={formData.agree}
        onChange={(checked) => handleChange('agree', checked)}
      />
      <button type="submit">提交</button>
    </form>
  );
}

三、设计的最高境界

在面向对象软件系统中,优秀的设计模式一定不能违反设计原则,恰当的设计模式能使软件系统的结构变得更加合理,让软件模块间的耦合度大大降低,从而提升系统的灵活性与扩展性,使我们可以在保证最小改动或者不做改动的前提下,通过增加模块的方式对系统功能进行增强。相较于简单的代码堆叠,设计模式能让系统以一种更为优雅的方式解决现实问题,并有能力应对不断扩展的需求。

随着业务需求的变动,系统设计并不是一成不变的。在设计原则的指导下,我们可以对设计模式进行适度地改造、组合,这样才能应对各种复杂的业务场景。然而,设计模式绝不可以被滥用,以免陷入“为了设计而设计”的误区,导致过度设计。例如一个相对简单的系统功能也许只需要几个类就能够实现,但设计者生搬硬套各种设计模式,拆分出几十个模块,结果适得其反,不切实际的模式堆砌反而会造成系统性能瓶颈,变成一种拖累。

世界上并不存在无所不能的设计,而且任何事物都有其两面性,任何一种设计模式都有其优缺点,所以对设计模式的运用一定要适可而止,否则会使系统臃肿不堪。满足目前需求,并在未来可预估业务范围内的设计才是最合理的设计。当然,在系统不能满足需求时我们还可以做出适当的重构,这样的设计才是切合实际的。

虽然不同的设计模式是为了解决不同的问题,但它们之间有很多类似且相通的地方,即便作为“灵魂本质”的设计原则之间也有着千丝万缕的关联,它们往往是相辅相成、互相印证的,所以我们不必过分纠结,避免机械式地将它们分门别类、划清界限。在工作中,我们一定要合理地利用设计模式去解决目前以及可以预见的未来所面临的问题,并基于设计原则,不断反复思考与总结。直到有一天,我们可能会忘记这些设计模式的名字,突破了“招式”和“套路”的牵绊,最终达到一种融会贯通的状态,各种“组合拳”信手拈来、运用自如。当各种模式在我们的设计中变得“你中有我,我中有你”时,才达到了不拘泥于任何形式的境界。