跳到主要内容

常用编程模式:前端高频模式、适用场景与面试答法

0. 面试速答(30 秒版 TL;DR)

前端里说“编程模式”,不要背一长串 GoF 名词,更好的答法是先按“解决什么问题”分类:

  • 分支很多,想把 if/else 拆开:用策略模式(Strategy)
  • 一个状态变化,要通知多个模块:用观察者模式(Observer)发布订阅模式(Publish-Subscribe)
  • 创建逻辑复杂,不想在业务里到处 new:用工厂模式(Factory)
  • 全局只需要一个共享实例:谨慎用单例模式(Singleton)
  • 想在不改原对象核心逻辑的前提下增强能力:用装饰器模式(Decorator)
  • 新旧接口不兼容,但又不想大改调用方:用适配器模式(Adapter)

一句话总结:

编程模式不是“高级语法”,而是对常见变化点的稳定封装。模式的价值,在于把易变部分隔离出来,让代码更容易扩展、替换和测试。


1. 心智模型:先别问“这是什么模式”,先问“变化点在哪里”

很多人学模式会陷入一个误区:先记名字,再强行往代码里套。这样很容易把简单问题写复杂。

更实用的思路是先识别代码里的“变化点”:

  • 某个功能分支会持续增加吗?
  • 某个模块的结果会影响很多下游模块吗?
  • 某个对象的创建流程是否越来越重?
  • 某个能力是否需要按场景动态拼装?
  • 某个第三方接口是否和现有系统不兼容?

如果你能先说清楚变化点,再说模式,面试官通常会认为你是真的理解了,而不是死记硬背。


2. 前端最常见的 6 个模式

2.1 策略模式(Strategy)

它解决什么问题?

当一段业务逻辑里充满了“按条件选择不同算法/规则”的代码时,策略模式可以把不同规则拆成独立实现,再在运行时选择其中一个。

典型场景:

  • 表单校验规则切换
  • 支付方式处理
  • 列表排序规则切换
  • 不同端或不同会员等级的定价逻辑

最小示例

type PriceStrategy = (price: number) => number;

const strategies: Record<string, PriceStrategy> = {
normal(price) {
return price;
},
vip(price) {
return price * 0.9;
},
superVip(price) {
return price * 0.8;
},
};

function calcPrice(level: string, price: number) {
const strategy = strategies[level] ?? strategies.normal;
return strategy(price);
}

面试里怎么说

  • 本质是把“会变化的算法”从主流程里抽走
  • 新增策略通常只需要加实现,不必改主流程
  • 它符合开闭原则(Open-Closed Principle):对扩展开放,对修改关闭

适合什么场景?

  • 分支会继续增长
  • 各分支彼此独立
  • 调用方只关心“结果”,不关心内部实现

常见误区

  • 如果只有 2 个非常稳定的分支,直接 if/else 可能更简单
  • 不要为了“有模式”而把极小逻辑拆成几十个策略文件

2.2 观察者模式(Observer)

它解决什么问题?

当一个对象状态变化后,需要自动通知多个依赖方时,观察者模式很自然。

前端里很常见的影子:

  • Vue/React 响应式更新链路
  • 状态管理中的订阅机制
  • 表单字段联动
  • 组件内部状态变化后的多处响应

最小示例

type Listener = (value: string) => void;

class Subject {
private listeners = new Set<Listener>();
private state = "";

subscribe(listener: Listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}

setState(value: string) {
this.state = value;
this.listeners.forEach((listener) => listener(this.state));
}
}

const subject = new Subject();
subject.subscribe((value) => console.log("header update:", value));
subject.subscribe((value) => console.log("sidebar update:", value));

subject.setState("dark");

面试里怎么说

  • 观察者强调的是对象之间的直接订阅关系
  • 被观察者通常知道订阅者列表
  • 状态变化后主动逐个通知

常见风险

  • 忘记取消订阅,容易造成内存泄漏
  • 订阅链太多时,数据流会变得难追踪

2.3 发布订阅模式(Publish-Subscribe)

它和观察者有什么区别?

这是高频追问。

两者都解决“通知”问题,但耦合层级不同:

  • 观察者模式:订阅者直接挂在目标对象上,目标对象知道“我有哪些观察者”
  • 发布订阅模式:中间有一个事件中心,发布者和订阅者彼此不知道对方,只认识中介

最小示例

type Handler = (payload: unknown) => void;

class EventBus {
private events = new Map<string, Set<Handler>>();

on(eventName: string, handler: Handler) {
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
}
this.events.get(eventName)!.add(handler);
return () => this.events.get(eventName)?.delete(handler);
}

emit(eventName: string, payload: unknown) {
this.events.get(eventName)?.forEach((handler) => handler(payload));
}
}

前端真实场景

  • 全局事件总线
  • 埋点通知
  • 微前端子应用之间通信
  • 编辑器插件系统

面试标准答法

观察者和发布订阅都能做通知,但发布订阅多了一层事件中心,所以发布者和订阅者解耦更彻底;代价是事件流更隐式,排查问题时不如直接依赖直观。

常见坑

  • 全局 Event Bus 很容易失控,最终变成“谁都能发、谁都能收”的黑盒
  • 事件名散落字符串,长期会带来维护风险,最好做常量化或类型约束

2.4 工厂模式(Factory)

它解决什么问题?

当对象创建过程不仅仅是 new Xxx(),而是伴随参数校验、默认配置、环境差异、依赖注入时,工厂模式可以把创建细节统一收口。

前端里很常见的场景:

  • 根据运行环境创建请求实例
  • 根据组件类型返回不同渲染器
  • 根据业务配置创建图表实例、编辑器实例

最小示例

interface RequestClient {
request(url: string): Promise<unknown>;
}

class FetchClient implements RequestClient {
request(url: string) {
return fetch(url).then((res) => res.json());
}
}

class MockClient implements RequestClient {
request(url: string) {
return Promise.resolve({ url, mock: true });
}
}

function createRequestClient(mode: "prod" | "mock"): RequestClient {
if (mode === "mock") return new MockClient();
return new FetchClient();
}

面试里怎么说

  • 工厂模式把“创建逻辑”集中管理
  • 调用方只依赖抽象接口,不依赖具体类名和创建细节
  • 这对测试很友好,因为更容易替换实现

什么时候特别适合?

  • 创建逻辑复杂
  • 需要按环境或配置返回不同实例
  • 想降低调用方对具体实现的感知

2.5 单例模式(Singleton)

它解决什么问题?

某些资源在系统里只应该存在一个共享实例,比如:

  • 全局配置中心
  • 日志上报器
  • 浏览器端唯一 WebSocket 连接管理器
  • 全局缓存管理器

最小示例

class ConfigCenter {
private static instance: ConfigCenter | null = null;
private constructor(public readonly baseURL: string) {}

static getInstance() {
if (!ConfigCenter.instance) {
ConfigCenter.instance = new ConfigCenter("/api");
}
return ConfigCenter.instance;
}
}

const configA = ConfigCenter.getInstance();
const configB = ConfigCenter.getInstance();
console.log(configA === configB); // true

面试里要主动补一句

单例不是“高级”,它只是用共享换方便。它的副作用也很明显:

  • 隐式全局状态变多
  • 测试隔离更难做
  • 生命周期难管理
  • 并发或多实例需求出现时很难演进

更稳的表达方式

单例适合“天然唯一”的资源管理器,不适合把普通业务对象都做成全局实例。


2.6 装饰器模式(Decorator)

它解决什么问题?

当你想增强对象能力,但又不想直接改原实现时,可以把增强逻辑包在外层。

前端里常见表现:

  • 对请求函数统一加重试、鉴权、日志
  • 对组件统一加 loading、权限、埋点能力
  • 对方法做缓存、节流、防抖包装

最小示例

async function request(url: string) {
const response = await fetch(url);
return response.json();
}

function withLog<T extends (...args: any[]) => any>(fn: T) {
return async (...args: Parameters<T>): Promise<Awaited<ReturnType<T>>> => {
console.log("request start:", args);
const result = await fn(...args);
console.log("request end");
return result;
};
}

const requestWithLog = withLog(request);

面试里怎么说

  • 装饰器模式强调“在不改原对象核心职责的前提下做能力增强”
  • 比继承更灵活,因为增强可以按需叠加
  • 很多高阶函数(HOF)、高阶组件(HOC)、中间件,本质上都带有装饰器思想

常见误区

  • 多层包装后,调用链会变深,调试成本上升
  • 如果增强逻辑已经开始接管核心控制流,就不再只是“装饰”,而是重写职责了

3. 适配器模式(Adapter):前端落地也很高频

虽然很多人背“常用模式”时会漏掉它,但前端工程里其实经常遇到。

3.1 典型场景

  • 后端新旧接口字段不一致
  • 第三方 SDK 返回结构和内部领域模型不一致
  • 浏览器 API 和业务层约定不一致

3.2 最小示例

type ServerUser = {
user_name: string;
avatar_url: string;
};

type UserCardModel = {
name: string;
avatar: string;
};

function adaptUser(data: ServerUser): UserCardModel {
return {
name: data.user_name,
avatar: data.avatar_url,
};
}

3.3 面试答法

  • 适配器的核心不是“封装”,而是“把不兼容接口转换成目标接口
  • 它最大的价值是把兼容性代码收敛在边界层,而不是污染业务层

4. 前端里怎么选模式?别按书本章节选,按问题选

遇到的问题更常见的模式一句话判断
if/else 越写越多策略模式把可替换规则拆出去
一个状态变化影响多个模块观察者模式目标对象自己维护订阅者
模块之间不想直接互相引用发布订阅模式用事件中心解耦
创建对象步骤越来越复杂工厂模式把构建逻辑统一收口
只允许一个共享实例单例模式适合天然全局资源
想增强能力但不改核心逻辑装饰器模式用包装而不是侵入修改
新旧接口对不上适配器模式在边界层做转换

一个很适合面试的表达是:

我一般不会先说“我要用某某模式”,而是先看问题属于“分支膨胀、通知联动、创建复杂、能力增强还是接口兼容”,再去选对应模式。


5. 高频面试题 & 标准答法

Q1:观察者模式和发布订阅模式的区别是什么?

答法要点:

  • 两者都解决通知问题
  • 观察者是直接依赖,目标对象知道订阅者
  • 发布订阅通过事件中心中转,双方解耦更彻底
  • 发布订阅更灵活,但事件流更隐式

Q2:为什么说“组合优于继承”?

答法建议:

  • 继承是强耦合,父类变动会影响子类
  • 组合更像“拼能力”,装饰器、策略都体现了组合思想
  • 前端业务变化快,组合通常比继承更容易扩展

Q3:工厂模式和简单 new 的区别在哪?

答法要点:

  • new 只是实例化动作
  • 工厂模式关注的是“创建逻辑的统一管理”
  • 当创建过程包含环境判断、默认参数、依赖注入时,工厂价值就明显了

Q4:单例模式为什么经常被批评?

答法要点:

  • 它引入隐式共享状态
  • 会增加测试难度和耦合度
  • 很多看似“方便”的单例,后期都会成为演进阻力

Q5:前端框架里有哪些模式影子?

可答:

  • 响应式系统里的观察者思想
  • 组件库按 props 或配置选择实现时的策略思想
  • 请求库封装中的工厂和装饰器思想
  • 插件系统里的发布订阅思想
  • 兼容层和 BFF 映射里的适配器思想

6. 易错点/坑

  • 不要把模式当成目标,模式只是整理复杂度的手段
  • 不要为了“面向对象味道更浓”就把简单逻辑拆成过度抽象
  • 不要把 Event Bus 当万能解法,很多时候显式依赖和清晰调用链更重要
  • 单例要谨慎,尤其是会携带状态的实例
  • 策略模式适合“规则可替换”,不适合把一切业务都拆成碎片函数
  • 适配器最好放在边界层,例如 API 层、SDK 封装层,而不是散落在页面组件里

7. 一个更接近实战的总结

如果你是做前端业务开发,最值得优先掌握的不是 23 种设计模式全家桶,而是下面这几个判断:

  1. 分支逻辑会不会继续膨胀?
  2. 模块之间要不要解耦通知关系?
  3. 创建逻辑是否应该收口?
  4. 能力增强是该改原实现,还是外层包装?
  5. 兼容层应该放在哪一层最合适?

能把这 5 个问题答清楚,基本就已经不是“会背模式”,而是“会用模式”了。


8. 速记要点(可背诵)

  • 策略模式:拆算法,消灭分支膨胀
  • 观察者模式:目标对象直接通知订阅者
  • 发布订阅模式:借事件中心解耦
  • 工厂模式:统一管理创建逻辑
  • 单例模式:共享唯一实例,但要警惕全局状态
  • 装饰器模式:不改核心逻辑,外层增强能力
  • 适配器模式:把不兼容接口转成目标接口