跳到主要内容

设计模式热门面试题

使用说明

本篇共 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 的区别?

答案

对比项ProxyObject.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 } 形式的结果,就满足迭代器协议。donefalse 表示还有值,为 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...offor...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)是一种数学计算模型,由有限个状态、事件、转换规则和初始状态组成。在任意时刻,系统只能处于一个确定的状态,接收到事件后按照预定义的规则转换到下一个状态。

前端常见应用场景:

  1. 数据请求idle → loading → success / error,防止在 loading 时重复发请求
  2. 表单状态pristine → dirty → validating → submitting → submitted
  3. UI 组件:弹窗(closed → opening → open → closing)、下拉菜单、Toast
  4. 业务流程:购物车→结算→支付→完成、审批流程
  5. 游戏逻辑:角色状态(idle → walking → jumping → attacking)
  6. 连接管理:WebSocket 状态(disconnected → connecting → connected → reconnecting)
  7. 动画控制:多步骤动画的状态管理

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: 模板方法中"抽象方法"和"钩子方法"的区别?

答案

对比抽象方法钩子方法
是否必须实现必须,否则编译报错可选,有默认实现
默认实现有(通常为空或返回默认值)
语义"你必须告诉我怎么做""你可以选择性地干预"
TypeScriptabstract 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 + useReducerContext Provider消费 Context 的子组件
微前端通信GlobalMediator各子应用
MVC 架构ControllerModel 和 View
聊天室/IMChatRoom / 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: 观察者模式在前端框架中有哪些应用?

答案

  1. Vue 响应式系统

    // 简化版原理
    class Dep {
    private subscribers = new Set<Watcher>();

    depend() {
    if (activeWatcher) {
    this.subscribers.add(activeWatcher);
    }
    }

    notify() {
    this.subscribers.forEach(watcher => watcher.update());
    }
    }
  2. React 状态管理(如 MobX)

    const store = observable({ count: 0 });
    autorun(() => console.log(store.count)); // 自动订阅
    store.count++; // 自动触发
  3. 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(); // 模块加载时创建

相关链接