设计模式热门面试题
使用说明
本篇共 45 道题。
设计模式面试不应背定义和 UML,而要说明它解决的变化点、使用代价和项目实例。模式是沟通词汇,不是越多越好。
Q1: 什么是设计模式?
答案:
设计模式是对重复设计问题及其取舍的经验总结,提供角色和协作方式,而不是可直接复制的代码模板。
使用前应先确认变化点真实存在。简单逻辑为了套模式增加多层抽象,会降低可读性。
- 设计模式描述的是特定上下文中反复出现的问题、角色关系和取舍,不是可直接复制的代码模板。
- 回答时要同时说明它隔离了哪个变化点、带来什么额外抽象,以及在当前语言或框架中是否已有更简单的能力。
Q2: 观察者模式和发布订阅模式有什么区别?
答案:
| 对比项 | 观察者模式 | 发布订阅模式 |
|---|---|---|
| 耦合度 | Subject 和 Observer 直接关联 | 通过事件中心完全解耦 |
| 知道对方 | Subject 知道 Observer | 发布者和订阅者互不知道 |
| 中介 | 无 | 事件中心(Event Channel) |
| 通信方式 | 同步 | 可同步可异步 |
观察者模式:Subject ←→ Observer(直接通信)
发布订阅:Publisher → Event Center → Subscriber(间接通信)
Q3: 单例模式有哪些问题?
答案:
它保证一个作用域内只有一个实例并提供访问点,但容易形成隐藏全局状态、测试污染、并发初始化和生命周期不清。
连接池、配置等可由依赖注入容器管理作用域,比模块里随意导出可变单例更易测试。
- 单例把创建和全局访问绑定在一起,容易形成隐藏依赖、共享可变状态和测试污染,多线程或多实例环境也未必真是“全局唯一”。
- 前端常可用 ES Module 单实例;需要生命周期和替换能力时,依赖注入通常比到处调用
getInstance()更容易维护。
Q4: 工厂模式适合什么场景?
答案:
当创建过程复杂、具体实现会变化或调用方不应依赖构造细节时,由工厂根据配置返回统一契约的对象。
例如按文件类型创建解析器。只有一个简单构造且没有变化时,工厂会增加不必要跳转。
- 当创建逻辑复杂、对象类型需要按配置变化,或调用方不应依赖具体类时使用工厂。它把“创建什么、如何创建”从业务流程中隔离。
- 如果只是调用一次构造函数,增加工厂只会多一层跳转;随着产品族增加,再从简单工厂演进到工厂方法或抽象工厂。
Q5: 策略模式解决什么问题?
答案:
把一组可互换算法封装成相同接口,由上下文在运行时选择,例如价格、校验或重试策略。
它减少大段条件分支并便于测试,但策略太细会产生大量小对象。简单映射表有时已经足够。
- 策略模式把可替换算法放到统一接口后,调用方根据上下文选择策略,适合支付、排序、校验和折扣规则。
- 它减少大段条件分支,但策略很多时要管理注册、默认值和组合关系;简单的两三个无增长分支保留普通函数往往更清楚。
Q6: 代理模式和装饰器模式有什么区别?
答案:
| 对比项 | 代理模式 | 装饰器模式 |
|---|---|---|
| 目的 | 控制访问 | 增强功能 |
| 接口 | 相同接口 | 相同接口 |
| 创建时机 | 代理创建对象 | 包装已有对象 |
| 典型应用 | 延迟加载、权限 | 日志、缓存 |
Q7: 适配器模式解决什么问题?
答案:
它把现有接口转换成调用方期望的接口,用于第三方库、旧系统迁移和多供应商统一。
适配器应把差异限制在边界,不能把供应商专属概念泄漏到领域层;错误、能力缺失和性能差异也要明确表达。
- 适配器把已有接口转换成调用方期望的接口,适合第三方 SDK、旧系统迁移和多数据源统一。
- 转换应集中在边界层,并明确字段缺失、错误码和能力差异;不能为了表面统一而隐藏某个提供方无法支持的语义。
Q8: 状态模式和普通条件判断怎么选?
答案:
状态少、转换简单时条件判断更直观;状态多、每个状态行为不同且转换规则复杂时,状态模式把行为放到状态对象或显式状态机中。
无论哪种实现,都应定义合法转换、事件和不可达状态,避免只把 switch 拆成多个文件。
- 状态模式适合对象行为随有限状态显著变化、转换规则复杂的场景,它让每个状态封装自己的行为和迁移。
- 如果只有少量稳定判断,普通条件更直接;引入后应画出状态机,限制非法转换,避免状态类之间随意互调。
Q9: 命令模式有什么价值?
答案:
把一次操作封装成对象,使调用者与执行者解耦,并可以排队、记录、重试、撤销或组合命令。
编辑器撤销、任务队列和快捷键映射常见。撤销需要保存反向操作或状态快照,并处理不可逆副作用。
- 命令把请求封装成对象,可携带参数并支持排队、日志、撤销、重放和组合,编辑器历史、任务队列很常见。
- 撤销通常需要保存旧状态或定义反向命令;涉及外部副作用时不能假设完全可逆,还要设计幂等和补偿。
Q10: 组合模式适合什么数据?
答案:
当单个对象和对象组合需要统一操作时使用树形组合,例如菜单、文件系统和 UI 节点。
调用方可统一遍历叶子与容器,但类型系统要表达哪些操作只对容器有效,不能为了统一接口强迫叶子实现无意义方法。
- 组合模式用统一接口对待叶子和容器,适合菜单、文件树、组织架构与 UI 节点;调用方可递归遍历而无需区分节点类型。
- 需要防止循环引用和无限深度,并明确哪些操作对容器与叶子都合理,避免“统一接口”里出现大量无效方法。
Q11: 模板方法和策略模式有什么区别?
答案:
核心区别是继承 vs 组合、改变部分步骤 vs 替换整个算法:
// 模板方法:改变算法的"某些步骤"
// 适用场景:流程固定,只是某些步骤不同
abstract class Authenticator {
login(credentials: Credentials): void {
this.validate(credentials); // 通用
this.authenticate(credentials); // 不同平台不同实现
this.onSuccess(); // 通用
}
protected abstract authenticate(credentials: Credentials): void;
}
class OAuthAuthenticator extends Authenticator {
protected authenticate(credentials: Credentials): void {
// OAuth 认证
}
}
// 策略模式:替换"整个算法"
// 适用场景:算法之间完全独立,运行时可切换
interface SortStrategy<T> {
sort(data: T[]): T[];
}
class QuickSort<T> implements SortStrategy<T> {
sort(data: T[]): T[] { /* 快排 */ return data; }
}
class MergeSort<T> implements SortStrategy<T> {
sort(data: T[]): T[] { /* 归并 */ return data; }
}
// 运行时切换策略
const sorter = new DataSorter(new QuickSort());
sorter.setStrategy(new MergeSort()); // 动态切换
选择建议:
- 有固定流程 + 部分步骤可变 -> 模板方法
- 算法完全独立 + 需要运行时切换 -> 策略模式
Q12: 中介者模式有什么优缺点?
答案:
中介者集中协调多个对象,减少它们两两依赖,例如表单字段联动和复杂 UI 协作。
优点是组件更独立,风险是中介者变成包含全部业务的“上帝对象”。应按领域拆分协调职责。
- 中介者把多对象之间的网状通信集中为星状协调,表单联动、弹窗管理和工作流编排都可能使用。
- 优点是对象解耦,风险是中介者演变成上帝对象;应按领域拆分中介者,并让业务规则仍有清晰归属。
Q13: 享元模式适合什么场景?
答案:
大量细粒度对象共享相同的内在状态时,把共享部分集中复用,外部状态由调用方传入,降低内存和创建成本。
例如编辑器字形、地图标记样式。只有测量证明对象数量和内存是瓶颈时才值得增加状态拆分复杂度。
- 享元把大量对象中不变、可共享的内部状态提取出来,外部状态由调用方在使用时传入,常见于文本字形、地图标记和粒子。
- 只有对象数量大且共享比例高时才值得使用;共享对象必须不可变或受控,否则一个修改会影响所有使用者。
Q14: 依赖注入算什么模式?
答案:
它把依赖从对象内部创建改为由外部提供,是控制反转的一种实现,能降低具体实现耦合并提升测试替换能力。
简单项目用构造参数即可,不必一定引入容器。容器过度使用会让依赖关系隐藏和运行时错误增多。
- 依赖注入更像控制反转的实现方式:对象声明需要什么依赖,由外部容器或组合根负责创建和传入。
- 它提升可替换性和测试性,但容器配置过度会隐藏真实依赖;优先构造函数注入,并把组装集中在应用边界。
Q15: 如何判断是否应该引入一个设计模式?
答案:
先看变化频率、重复分支、依赖方向和测试痛点,再比较直接实现与模式的认知成本。
好的模式让主要流程更容易理解,并把变化限制在清晰边界;如果只能用模式术语解释、却不能减少实际复杂度,就不应引入。
- 先确认问题是否反复出现、变化方向是否稳定,再看模式能否减少调用方知识或修改范围。
- 引入前比较普通函数、组合和框架内建能力;引入后如果类数量、跳转层级和学习成本大于变化收益,就应退回更简单设计。
Q16: 责任链模式适合解决什么问题?
答案:
责任链把多个处理器按顺序连接,每个处理器决定处理、拒绝或把请求交给下一个。它适合中间件、校验链、审批流和事件拦截器。
优点是发送者不必知道最终由谁处理,步骤可组合和插拔;风险是控制流不直观、顺序敏感、请求可能无人处理或被重复传递。
工程上应定义清晰的上下文、终止语义、错误传播和可观测日志。若所有步骤始终必须执行,普通流水线可能比“可跳过的责任链”更清晰。
Q17: 外观模式(Facade)在前端工程中有什么价值?
答案:
外观模式为多个复杂子系统提供一个稳定、简化的入口。例如上传模块可以在内部组合签名、分片、重试、进度和埋点,对业务只暴露 upload(file)。
它能降低调用方耦合并集中兼容处理,但不能演变成无边界的“万能 Service”。外观层应只编排公开用例,领域规则仍放在对应模块,底层高级能力也可为少量专业调用方保留。
当第三方 SDK 更换频繁或多端实现不同,Facade 还可以作为反腐层稳定业务接口。
Q18: 简单工厂、工厂方法、抽象工厂的区别?
答案:
// 1. 简单工厂 - 一个工厂创建所有产品
class SimpleFactory {
static create(type: string) {
if (type === 'A') return new ProductA();
if (type === 'B') return new ProductB();
}
}
// 2. 工厂方法 - 每个产品对应工厂
interface Factory {
create(): Product;
}
class ProductAFactory implements Factory {
create() {
return new ProductA();
}
}
// 3. 抽象工厂 - 创建产品家族
interface UIFactory {
createButton(): Button;
createInput(): Input;
}
class MacUIFactory implements UIFactory {
createButton() {
return new MacButton();
}
createInput() {
return new MacInput();
}
}
| 对比项 | 简单工厂 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| 复杂度 | 低 | 中 | 高 |
| 扩展性 | 需修改工厂 | 新增工厂类 | 新增工厂类 |
| 产品关系 | 单一产品 | 单一产品 | 产品家族 |
| 适用场景 | 产品类型少 | 产品扩展多 | 多系列产品 |
Q19: 策略模式的优缺点?
答案:
| 优点 | 缺点 |
|---|---|
| 消除大量条件判断 | 增加策略类数量 |
| 符合开闭原则 | 客户端需要知道所有策略 |
| 算法可复用 | 策略间不能通信 |
| 运行时动态切换 | - |
Q20: Proxy 和 Object.defineProperty 的区别?
答案:
| 对比项 | Proxy | Object.defineProperty |
|---|---|---|
| 拦截范围 | 13 种操作 | get/set |
| 数组支持 | 原生支持索引、length | 需要特殊处理 |
| 新属性 | 自动拦截 | 需要手动添加 |
| 性能 | 略低(创建代理) | 略高 |
| 兼容性 | IE 不支持 | IE9+ |
| 深层代理 | 需要递归 | 需要递归 |
// Object.defineProperty
const obj: Record<string, unknown> = {};
Object.defineProperty(obj, 'name', {
get() {
return this._name;
},
set(value) {
this._name = value;
},
});
// Proxy
const proxy = new Proxy({}, {
get(target, key) {
return Reflect.get(target, key);
},
set(target, key, value) {
return Reflect.set(target, key, value);
},
});
Q21: 装饰器模式的优缺点?
答案:
| 优点 | 缺点 |
|---|---|
| 动态扩展功能 | 增加系统复杂度 |
| 比继承更灵活 | 多层装饰难以调试 |
| 符合开闭原则 | 装饰顺序可能影响结果 |
| 单一职责 | - |
Q22: 适配器模式的优缺点?
答案:
| 优点 | 缺点 |
|---|---|
| 解耦客户端与接口 | 增加系统复杂度 |
| 提高类的复用性 | 过多适配器难以维护 |
| 灵活性好 | 可能影响性能 |
| 符合开闭原则 | - |
Q23: 什么是迭代器协议和可迭代协议?它们之间有什么关系?
答案:
迭代器协议(Iterator Protocol):一个对象实现了 next() 方法,该方法返回 { value, done } 形式的结果,就满足迭代器协议。done 为 false 表示还有值,为 true 表示迭代结束。
可迭代协议(Iterable Protocol):一个对象实现了 [Symbol.iterator]() 方法,该方法返回一个满足迭代器协议的对象,就满足可迭代协议。
两者的关系:可迭代协议是"工厂",每次调用 [Symbol.iterator]() 创建一个新的迭代器;迭代器协议是"游标",负责实际的遍历逻辑。
// 可迭代对象(实现 Symbol.iterator)
const iterable = {
[Symbol.iterator](): Iterator<number> {
let i = 0;
// 返回迭代器(实现 next)
return {
next(): IteratorResult<number> {
return i < 3
? { value: ++i, done: false }
: { value: undefined as any, done: true };
},
};
},
};
// 每次 for...of 都会创建新的迭代器
for (const n of iterable) console.log(n); // 1, 2, 3
for (const n of iterable) console.log(n); // 1, 2, 3(重新开始)
Q24: 状态模式的优缺点分别是什么?
答案:
| 优点 | 缺点 |
|---|---|
| 消除大量 if-else / switch 条件判断 | 状态类数量增多,代码量增大 |
| 符合开闭原则,新增状态只需添加新类 | 状态转换逻辑分散在各个状态类中,不如转换表直观 |
| 每个状态的行为内聚,职责清晰 | 简单场景下过度设计 |
| 状态转换有约束,不会出现非法状态 | 状态类之间可能存在耦合 |
Q25: 备忘录模式如何支持编辑器草稿和状态恢复?
答案:
备忘录模式把对象某一时刻的状态保存为快照,并在不暴露内部实现的前提下恢复。编辑器可在关键操作、自动保存或路由切换前生成快照,Caretaker 只保存和选择快照,Originator 负责创建与恢复。
工程上要控制成本:
- 大状态优先存增量、结构共享或操作日志,不要每次深拷贝整个文档。
- 给快照设置数量、时间和存储空间上限。
- 恢复前校验 Schema 与版本。
- 敏感内容持久化时考虑加密、权限和过期。
- 需要协作合并或精确撤销语义时,命令日志、OT/CRDT 可能更合适。
Q26: 桥接模式和适配器模式有什么区别?
答案:
桥接模式在设计阶段主动把两个会独立变化的维度拆成抽象与实现,通过组合自由搭配,例如“图表类型 × 渲染器”;适配器模式通常在已有接口不兼容时,加一层转换让旧实现满足新接口。
- 避免维度组合造成类爆炸并允许双方独立演进,用桥接。
- 接入第三方、旧系统或不同数据格式,用适配器。
- 桥接两侧通常是长期抽象;适配器更像兼容边界。
两者结构上都可能表现为“持有另一个对象并转发”,区别主要在设计意图和演进阶段。
Q27: 什么是模板方法模式?它解决了什么问题?
答案:
模板方法模式在父类中定义算法的骨架(即执行步骤的顺序),将具体步骤的实现延迟到子类。它解决的核心问题是代码复用和流程统一:
- 多个子类有相同的执行流程,但某些步骤的实现不同
- 希望流程只定义一次,避免各子类各自维护导致不一致
- 需要在固定流程中预留扩展点(钩子)
abstract class OrderProcessor {
// 模板方法:固定流程
process(order: Order): void {
this.validate(order); // 步骤1:校验
this.calculatePrice(order); // 步骤2:计算价格(抽象)
this.applyDiscount(order); // 步骤3:折扣(钩子,可选)
this.submit(order); // 步骤4:提交
}
protected abstract calculatePrice(order: Order): void;
protected applyDiscount(order: Order): void {} // 钩子
private validate(order: Order): void { /* 通用校验 */ }
private submit(order: Order): void { /* 通用提交 */ }
}
Q28: 什么是中介者模式?解决了什么问题?
答案:
中介者模式通过引入一个中介者对象,将多个对象之间的网状多对多通信转化为星型一对多通信。各对象(Colleague)不再直接引用彼此,而是通过中介者间接通信。
核心解决的问题是降低对象间的耦合度:
// 没有中介者:N 个对象两两通信,N*(N-1)/2 条连接
// 4 个组件 → 6 条连接
// 10 个组件 → 45 条连接
// 有中介者:N 个对象只需 N 条连接(各自只连中介者)
// 4 个组件 → 4 条连接
// 10 个组件 → 10 条连接
现实例子:机场塔台(中介者)协调所有飞机(Colleague)的起降,飞机之间不直接通信。
Q29: 原型模式在 JavaScript 中怎样使用?它等同于原型链吗?
答案:
原型模式的设计意图是通过复制已有对象创建新对象,适合初始化成本高、配置大部分相同的场景。JavaScript 原型链是语言级继承和属性查找机制,两者相关但不等同。
实现复制时要明确语义:
Object.create(proto)只创建以某对象为原型的新对象,没有复制自有状态。- 展开语法和
Object.assign只做浅拷贝。 structuredClone能复制许多内建结构,但不会保留自定义原型、函数和部分宿主对象。- 复杂领域对象更适合显式
clone()或工厂函数,定义哪些字段共享、哪些深拷贝。
不要把“使用了 prototype”就称为原型模式。
Q30: 手写一个 EventEmitter(发布订阅)
答案:
type EventHandler = (...args: any[]) => void;
class EventEmitter {
private events = new Map<string, EventHandler[]>();
// 订阅
on(event: string, handler: EventHandler): this {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(handler);
return this;
}
// 取消订阅
off(event: string, handler: EventHandler): this {
const handlers = this.events.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) handlers.splice(index, 1);
}
return this;
}
// 发布
emit(event: string, ...args: any[]): boolean {
const handlers = this.events.get(event);
if (!handlers?.length) return false;
handlers.forEach(h => h(...args));
return true;
}
// 只订阅一次
once(event: string, handler: EventHandler): this {
const wrapper: EventHandler = (...args) => {
handler(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}
Q31: JavaScript 中如何保证单例?
答案:
// 方法 1:ES Module 导出实例
export const instance = new MyClass();
// 方法 2:静态属性 + 私有构造函数
class Singleton {
private static instance: Singleton;
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
// 方法 3:闭包
const Singleton = (() => {
let instance: MyClass;
return {
getInstance: () => instance || (instance = new MyClass()),
};
})();
Q32: 工厂模式的优缺点?
答案:
| 优点 | 缺点 |
|---|---|
| 解耦创建和使用 | 增加类的数量 |
| 易扩展新产品 | 简单工厂违反开闭原则 |
| 符合单一职责 | 增加系统抽象性 |
| 隐藏实现细节 | - |
Q33: 策略模式与工厂模式的区别?
答案:
| 对比项 | 策略模式 | 工厂模式 |
|---|---|---|
| 目的 | 封装算法 | 封装创建 |
| 关注点 | 行为 | 对象 |
| 结果 | 执行策略 | 返回对象 |
| 使用方式 | 替换算法 | 创建实例 |
// 策略模式 - 关注算法执行
const strategy = strategies[type];
strategy.execute(data);
// 工厂模式 - 关注对象创建
const product = Factory.create(type);
product.doSomething();
Q34: 空对象模式有什么价值和风险?
答案:
空对象模式提供一个实现相同接口、但执行安全空行为的对象,减少调用方反复判断 null。例如未配置埋点时注入 NoopAnalytics,业务代码仍调用 track(),但不会发送数据。
它适合“缺省行为本来就是合法的不执行”,并能简化依赖注入和测试。风险是掩盖本应报错的配置缺失:支付网关、权限校验等关键依赖若悄悄变成 no-op,问题会更难发现。
空对象应有明确语义和可观测性,关键场景在启动阶段校验依赖;不要把所有异常吞掉后称为模式。
Q35: 建造者模式适合什么场景?和普通配置对象有什么区别?
答案:
建造者模式把复杂对象的分步构造封装起来,适合创建顺序、校验、默认值和多种最终表示都较复杂的场景,例如图表配置、查询构造器和测试数据。
普通配置对象更直接,字段不多且没有构造约束时应优先使用。Builder 的价值在于:
- 每一步提供类型安全的链式 API。
- 最终
build()前统一校验不变量。 - 复用部分构造步骤并隐藏底层对象结构。
- 可以生成不同输出。
如果只是把每个字段都包装成一个 setter,会增加样板代码而没有模式收益。
Q36: 适配器和装饰器的区别?
答案:
| 对比项 | 适配器模式 | 装饰器模式 |
|---|---|---|
| 目的 | 接口转换 | 功能增强 |
| 接口 | 转换为新接口 | 保持原接口 |
| 时机 | 设计后期 | 设计时考虑 |
| 结构 | 单层包装 | 可多层包装 |
// 适配器 - 接口转换
class Adapter implements NewInterface {
private adaptee: OldInterface;
newMethod() {
return this.adaptee.oldMethod(); // 转换调用
}
}
// 装饰器 - 功能增强
class Decorator implements Interface {
private wrapped: Interface;
method() {
// 增强前
const result = this.wrapped.method();
// 增强后
return result;
}
}
Q37: for...of 和 for...in 的区别是什么?
答案:
| 对比项 | for...of | for...in |
|---|---|---|
| 遍历目标 | 值(可迭代对象的元素) | 键(对象的可枚举属性名) |
| 适用对象 | 实现了 [Symbol.iterator] 的对象 | 任何对象 |
| 原型链 | 不涉及原型链 | 会遍历原型链上的可枚举属性 |
| 数组表现 | 遍历元素值 | 遍历索引字符串 |
| 普通对象 | 默认不可用(需自定义迭代器) | 可直接使用 |
const arr = ['a', 'b', 'c'];
for (const value of arr) console.log(value); // 'a', 'b', 'c'
for (const key in arr) console.log(key); // '0', '1', '2'
// for...in 的陷阱:会遍历原型链
(Array.prototype as any).foo = 'bar';
for (const key in arr) console.log(key); // '0', '1', '2', 'foo' ❌
选择原则:遍历数组/Map/Set 用 for...of,遍历对象属性用 Object.keys() + for...of,尽量避免 for...in。
Q38: 什么是有限状态机?它在前端有哪些应用场景?
答案:
有限状态机(FSM)是一种数学计算模型,由有限个状态、事件、转换规则和初始状态组成。在任意时刻,系统只能处于一个确定的状态,接收到事件后按照预定义的规则转换到下一个状态。
前端常见应用场景:
- 数据请求:
idle → loading → success / error,防止在 loading 时重复发请求 - 表单状态:
pristine → dirty → validating → submitting → submitted - UI 组件:弹窗(
closed → opening → open → closing)、下拉菜单、Toast - 业务流程:购物车→结算→支付→完成、审批流程
- 游戏逻辑:角色状态(idle → walking → jumping → attacking)
- 连接管理:WebSocket 状态(disconnected → connecting → connected → reconnecting)
- 动画控制:多步骤动画的状态管理
Q39: 如何实现撤销/重做?为什么执行新命令要清空 redoStack?
答案:
撤销/重做通过两个栈实现:
- undoStack:存储已执行的命令,
undo()时从中弹出 - redoStack:存储已撤销的命令,
redo()时从中弹出
执行新命令必须清空 redoStack,因为新操作产生了新的时间线,旧的"未来操作"已经不再有效。这与 Git 的分叉类似——你在历史中回退后做了新修改,之前被撤销的操作就无法再重做了。
class History {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
execute(cmd: Command): void {
cmd.execute();
this.undoStack.push(cmd);
this.redoStack = []; // 关键:新操作清空重做栈
}
undo(): void {
const cmd = this.undoStack.pop();
if (cmd) {
cmd.undo();
this.redoStack.push(cmd);
}
}
redo(): void {
const cmd = this.redoStack.pop();
if (cmd) {
cmd.execute();
this.undoStack.push(cmd);
}
}
}
Q40: 组合模式中的透明方式和安全方式如何选择?
答案:
区别在于叶子和组合节点是否暴露完全相同的子节点管理接口:
- 透明方式:Component 统一声明
add/remove/getChild,客户端可一致对待所有节点;但叶子上的这些操作没有合理语义,只能报错或空实现。 - 安全方式:只有 Composite 暴露管理接口,错误调用能更早被类型或接口阻止;代价是客户端修改树结构时必须区分节点类型。
选择依据是业务更看重统一遍历还是安全修改,而不是按 JavaScript/TypeScript 简单决定。只读遍历可放在基础接口,结构修改通过类型守卫、专门服务或组合节点接口暴露。
Q41: 模板方法中"抽象方法"和"钩子方法"的区别?
答案:
| 对比 | 抽象方法 | 钩子方法 |
|---|---|---|
| 是否必须实现 | 必须,否则编译报错 | 可选,有默认实现 |
| 默认实现 | 无 | 有(通常为空或返回默认值) |
| 语义 | "你必须告诉我怎么做" | "你可以选择性地干预" |
| TypeScript | abstract method() | 普通方法,提供空实现 |
| React 类比 | render()(必须实现) | componentDidMount()(可选) |
| Vue 类比 | setup()(函数式组件必须) | mounted()(可选) |
abstract class Component {
// 抽象方法 —— 不实现会报错
abstract render(): string;
// 钩子方法 —— 有默认实现,子类可选覆盖
shouldUpdate(): boolean {
return true;
}
onMounted(): void {
// 默认空实现
}
}
Q42: 前端开发中有哪些中介者模式的实际应用?
答案:
| 应用场景 | 中介者角色 | Colleague 角色 |
|---|---|---|
| 表单联动 | FormMediator | 各表单控件(省/市/区、日期/时间) |
| Vue EventBus / mitt | 事件中心 | 各 Vue 组件 |
| React Context + useReducer | Context Provider | 消费 Context 的子组件 |
| 微前端通信 | GlobalMediator | 各子应用 |
| MVC 架构 | Controller | Model 和 View |
| 聊天室/IM | ChatRoom / Server | 各用户客户端 |
| 状态管理库(Redux/Vuex) | Store | 各组件 |
其中,Redux 的 Store 也是典型的中介者 — 所有组件通过 dispatch 发送 Action 到 Store,Store 根据 Reducer 计算新状态并通知订阅者。
Q43: 如何区分内部状态和外部状态?
答案:
| 判断标准 | 内部状态 | 外部状态 |
|---|---|---|
| 多个对象间是否相同 | 相同 | 不同 |
| 是否随使用场景变化 | 不变 | 变化 |
| 能否被多对象共享 | 能 | 不能 |
常见的内部/外部状态划分:
// 文本编辑器中的字符
// 内部状态:字体、大小、样式(可能上千个字符用同一套)
// 外部状态:字符内容、位置(每个字符都不同)
interface CharFlyweight {
font: string; // 内部
size: number; // 内部
bold: boolean; // 内部
}
interface CharContext {
char: string; // 外部
row: number; // 外部
col: number; // 外部
flyweight: CharFlyweight; // 引用共享的享元
}
Q44: 观察者模式在前端框架中有哪些应用?
答案:
-
Vue 响应式系统
// 简化版原理
class Dep {
private subscribers = new Set<Watcher>();
depend() {
if (activeWatcher) {
this.subscribers.add(activeWatcher);
}
}
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
} -
React 状态管理(如 MobX)
const store = observable({ count: 0 });
autorun(() => console.log(store.count)); // 自动订阅
store.count++; // 自动触发 -
DOM 事件监听
element.addEventListener('click', handler);
// element 是 Subject,handler 是 Observer
Q45: 懒汉式和饿汉式的区别?
答案:
| 类型 | 创建时机 | 优点 | 缺点 |
|---|---|---|---|
| 懒汉式 | 首次使用时创建 | 延迟加载,节省资源 | 需要判断逻辑 |
| 饿汉式 | 类加载时创建 | 实现简单,线程安全 | 可能浪费资源 |
// 懒汉式
class LazySingleton {
private static instance: LazySingleton;
static getInstance() {
if (!LazySingleton.instance) {
LazySingleton.instance = new LazySingleton();
}
return LazySingleton.instance;
}
}
// 饿汉式(ES Module 导出)
export const instance = new MyClass(); // 模块加载时创建