主题
前端手写题深度解析
一、防抖 debounce
原理
防抖的核心思想是:在事件被触发后,延迟一段时间再执行回调,如果在延迟期间事件再次被触发,则重新计时。
生活中的类比:电梯关门。每当有人进入电梯,关门计时器就会重置。只有在最后一个人进入后,等待一段时间没有新人进来,电梯门才会关闭。
触发事件: ─A──B──C──────D──E────────────→ 时间轴
↑ ↑
延迟窗口: │← delay →│ │← delay →│
实际执行: C E
说明: A、B 被后续触发覆盖,C 在延迟窗口结束后执行
D 被 E 覆盖,E 在延迟窗口结束后执行适用场景:
- 搜索框输入联想(用户停止输入后再发请求)
- 窗口 resize 事件(调整结束后再重新计算布局)
- 按钮防重复点击
版本一:基础版
最简单的防抖实现,理解核心逻辑:
typescript
function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}关键点解析:
ReturnType<typeof setTimeout>兼容 Node.js 和浏览器环境(Node 中setTimeout返回Timeout对象而非number)fn.apply(this, args)确保this指向正确,比如在对象方法中使用防抖Parameters<T>保留原函数的参数类型
版本二:支持 leading 模式
有时候我们希望第一次触发立即执行,后续触发才进行防抖。比如按钮点击:第一次点击立即响应,之后的快速连击被忽略。
trailing 模式(默认):
触发: ─A──B──C──────────→
执行: C (延迟后执行最后一次)
leading 模式:
触发: ─A──B──C──────────→
执行: A (立即执行第一次,后续被忽略)
leading + trailing 模式:
触发: ─A──B──C──────────→
执行: A C (首次立即执行 + 延迟后执行最后一次)版本三:完整版(leading / trailing / cancel / flush)
typescript
interface DebounceOptions {
leading?: boolean;
trailing?: boolean;
}
interface DebouncedFunction<T extends (...args: any[]) => any> {
(this: ThisParameterType<T>, ...args: Parameters<T>): void;
cancel: () => void;
flush: () => void;
}
function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number,
options: DebounceOptions = {}
): DebouncedFunction<T> {
const { leading = false, trailing = true } = options;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastArgs: Parameters<T> | null = null;
let lastThis: ThisParameterType<T> | null = null;
let lastCallTime: number | undefined;
let lastInvokeTime = 0;
function invoke() {
if (lastArgs !== null && lastThis !== null) {
fn.apply(lastThis, lastArgs);
lastInvokeTime = Date.now();
lastArgs = null;
lastThis = null;
}
}
function startTimer() {
timer = setTimeout(() => {
timer = null;
if (trailing && lastArgs) {
invoke();
}
}, delay);
}
function shouldInvoke(time: number): boolean {
const timeSinceLastCall = lastCallTime === undefined ? 0 : time - lastCallTime;
return lastCallTime === undefined || timeSinceLastCall >= delay;
}
const debounced = function (
this: ThisParameterType<T>,
...args: Parameters<T>
) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking && timer === null) {
if (leading) {
invoke();
}
startTimer();
return;
}
if (timer) clearTimeout(timer);
startTimer();
} as DebouncedFunction<T>;
debounced.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
lastArgs = null;
lastThis = null;
lastCallTime = undefined;
lastInvokeTime = 0;
};
debounced.flush = function () {
if (timer) {
clearTimeout(timer);
timer = null;
invoke();
}
};
return debounced;
}边界情况处理
| 边界情况 | 处理方式 |
|---|---|
delay 为 0 | 仍然是异步执行(setTimeout 最小延迟约 4ms) |
leading 和 trailing 都为 false | 函数永远不会执行,这是一个需要注意的配置陷阱 |
| 组件卸载时未取消 | 可能导致内存泄漏或操作已销毁的 DOM,必须调用 cancel() |
this 绑定 | 使用 apply 确保 this 透传,箭头函数场景下 this 取决于外层 |
flush 时没有待执行的调用 | 直接忽略,不会报错 |
二、节流 throttle
原理
节流的核心思想是:在指定时间间隔内,无论事件触发多少次,只执行一次回调。
生活中的类比:地铁发车。无论站台上有多少乘客在等待,地铁每隔固定时间才发一班。
触发事件: ─A─B─C─D─E─F─G─H─────→ 时间轴
间隔窗口: |← interval →|← interval →|
实际执行: A E (每个间隔只执行一次)防抖 vs 节流的本质区别:
- 防抖:等用户"安静下来"再执行 → 侧重于结果
- 节流:按照固定频率执行 → 侧重于过程
适用场景:
- 滚动事件监听(固定频率计算位置)
- 鼠标移动事件(拖拽场景)
- 游戏中的技能冷却
版本一:时间戳版(leading)
首次触发立即执行,最后一次触发可能被丢弃:
typescript
function throttleByTimestamp<T extends (...args: any[]) => any>(
fn: T,
interval: number
): (...args: Parameters<T>) => void {
let lastInvokeTime = 0;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
const now = Date.now();
if (now - lastInvokeTime >= interval) {
fn.apply(this, args);
lastInvokeTime = now;
}
};
}特点:第一次触发立即执行,但如果最后一次触发距离上次执行不到一个 interval,则最后一次调用会被丢弃。
版本二:定时器版(trailing)
首次触发延迟执行,保证最后一次触发一定会执行:
typescript
function throttleByTimer<T extends (...args: any[]) => any>(
fn: T,
interval: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
};
}特点:第一次触发不会立即执行,而是等待 interval 后执行。如果在等待期间有新的触发,会被忽略。保证最后一次触发在 interval 后执行。
版本三:组合版(leading + trailing + cancel)
结合时间戳版和定时器版的优势:首次立即执行 + 最后一次一定执行。
typescript
interface ThrottleOptions {
leading?: boolean;
trailing?: boolean;
}
interface ThrottledFunction<T extends (...args: any[]) => any> {
(this: ThisParameterType<T>, ...args: Parameters<T>): void;
cancel: () => void;
}
function throttle<T extends (...args: any[]) => any>(
fn: T,
interval: number,
options: ThrottleOptions = {}
): ThrottledFunction<T> {
const { leading = true, trailing = true } = options;
let lastInvokeTime = 0;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastArgs: Parameters<T> | null = null;
let lastThis: ThisParameterType<T> | null = null;
function invoke() {
if (lastArgs !== null && lastThis !== null) {
fn.apply(lastThis, lastArgs);
lastInvokeTime = Date.now();
lastArgs = null;
lastThis = null;
}
}
const throttled = function (
this: ThisParameterType<T>,
...args: Parameters<T>
) {
const now = Date.now();
const remaining = interval - (now - lastInvokeTime);
lastArgs = args;
lastThis = this;
if (remaining <= 0 || remaining > interval) {
if (timer) {
clearTimeout(timer);
timer = null;
}
if (leading || lastInvokeTime !== 0) {
invoke();
} else {
lastInvokeTime = now;
}
} else if (!timer && trailing) {
timer = setTimeout(() => {
timer = null;
lastInvokeTime = leading ? Date.now() : 0;
invoke();
}, remaining);
}
} as ThrottledFunction<T>;
throttled.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
lastArgs = null;
lastThis = null;
lastInvokeTime = 0;
};
return throttled;
}边界情况处理
| 边界情况 | 处理方式 |
|---|---|
remaining > interval | 系统时间被修改导致,此时立即执行 |
leading: false 首次触发 | 不执行但记录时间,确保后续间隔计算正确 |
| 快速连续触发后停止 | trailing: true 保证最后一次调用不会丢失 |
leading 和 trailing 都为 false | 同防抖,函数永远不执行 |
interval 为 0 | 每次触发都执行,等价于没有节流 |
三、深拷贝 deepClone
原理
JavaScript 中的赋值操作对于引用类型只拷贝引用地址(浅拷贝),而深拷贝需要递归地复制对象的每一层属性,使得新对象与原对象完全独立。
浅拷贝:
original ──→ { a: 1, b: ──→ { c: 2 } }
↑
copy ──────→ { a: 1, b: ───┘ }
深拷贝:
original ──→ { a: 1, b: ──→ { c: 2 } }
copy ──────→ { a: 1, b: ──→ { c: 2 } } (完全独立的对象)JSON.parse(JSON.stringify()) 的局限
这是最简单的深拷贝方式,但存在严重局限:
typescript
const original = {
date: new Date(),
regex: /test/gi,
fn: () => {},
undef: undefined,
nan: NaN,
infinity: Infinity,
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
symbol: Symbol('test'),
};
const clone = JSON.parse(JSON.stringify(original));| 数据类型 | JSON 方式的结果 |
|---|---|
Date | 变成字符串 "2024-01-01T00:00:00.000Z" |
RegExp | 变成空对象 {} |
Function | 被丢弃 |
undefined | 被丢弃 |
NaN | 变成 null |
Infinity | 变成 null |
Map / Set | 变成空对象 {} |
Symbol | 被丢弃 |
| 循环引用 | 直接抛出 TypeError |
递归版:完整实现
typescript
function deepClone<T>(source: T, cache = new WeakMap()): T {
if (source === null || typeof source !== 'object') {
return source;
}
if (cache.has(source as object)) {
return cache.get(source as object) as T;
}
if (source instanceof Date) {
return new Date(source.getTime()) as T;
}
if (source instanceof RegExp) {
return new RegExp(source.source, source.flags) as T;
}
if (source instanceof Map) {
const mapClone = new Map();
cache.set(source as object, mapClone);
source.forEach((value, key) => {
mapClone.set(deepClone(key, cache), deepClone(value, cache));
});
return mapClone as T;
}
if (source instanceof Set) {
const setClone = new Set();
cache.set(source as object, setClone);
source.forEach((value) => {
setClone.add(deepClone(value, cache));
});
return setClone as T;
}
if (ArrayBuffer.isView(source)) {
const TypedArrayConstructor = source.constructor as new (
buffer: ArrayBuffer
) => T;
return new TypedArrayConstructor(
(source as unknown as { buffer: ArrayBuffer }).buffer.slice(0)
);
}
const clone = Array.isArray(source)
? []
: Object.create(Object.getPrototypeOf(source));
cache.set(source as object, clone);
const allKeys = [
...Object.keys(source as object),
...Object.getOwnPropertySymbols(source as object),
];
for (const key of allKeys) {
const descriptor = Object.getOwnPropertyDescriptor(source as object, key);
if (descriptor) {
if ('value' in descriptor) {
(clone as Record<string | symbol, unknown>)[key] = deepClone(
descriptor.value,
cache
);
} else {
Object.defineProperty(clone, key, descriptor);
}
}
}
return clone as T;
}循环引用处理详解
obj.self = obj 的情况:
WeakMap 记录: { obj → clone }
递归遍历 obj 的属性:
→ key: 'self', value: obj
→ cache.has(obj) === true
→ 返回 cache.get(obj) 即 clone
→ clone.self = clone ✓(正确的循环引用结构)使用 WeakMap 而非 Map 的原因:WeakMap 的 key 是弱引用,当原对象被垃圾回收时,WeakMap 中的条目也会自动清除,避免内存泄漏。
structuredClone 对比
structuredClone 是浏览器和 Node.js (v17+) 内置的深拷贝 API:
typescript
const clone = structuredClone(original);| 特性 | 手写 deepClone | structuredClone |
|---|---|---|
| 循环引用 | ✅ WeakMap | ✅ 原生支持 |
| Date / RegExp | ✅ | ✅ |
| Map / Set | ✅ | ✅ |
| ArrayBuffer / TypedArray | ✅ | ✅(可转移) |
| Function | ❌ 无法拷贝 | ❌ 抛出异常 |
| DOM 节点 | ❌ 需额外处理 | ❌ 抛出异常 |
| Symbol 属性 | ✅ 可处理 | ❌ 忽略 |
| 原型链 | ✅ 保留 | ❌ 丢失 |
| 属性描述符(getter/setter) | ✅ 可保留 | ❌ 丢失 |
| 性能 | 一般 | 优秀(原生实现) |
边界情况处理
| 边界情况 | 处理方式 |
|---|---|
null | 直接返回 null(typeof null === 'object') |
| 原始类型 | 直接返回值本身 |
| 循环引用 | WeakMap 缓存已创建的克隆对象 |
| Symbol 属性 | 使用 getOwnPropertySymbols 遍历 |
| getter / setter | 使用 getOwnPropertyDescriptor 保留描述符 |
| 原型链 | 使用 Object.create(Object.getPrototypeOf(source)) 保留 |
Map 的 key 是对象 | key 也需要深拷贝 |
TypedArray | 拷贝底层 ArrayBuffer |
四、数组扁平化 flat
原理
数组扁平化是将嵌套数组转换为一维数组的过程。核心问题是如何递归处理不确定层级的嵌套结构。
输入: [1, [2, [3, [4]], 5]]
depth=1: [1, 2, [3, [4]], 5]
depth=2: [1, 2, 3, [4], 5]
depth=Infinity: [1, 2, 3, 4, 5]版本一:递归版
typescript
function flatRecursive<T>(arr: T[], depth: number = 1): FlatArray<T[], number>[] {
const result: any[] = [];
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...flatRecursive(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}版本二:reduce 版
typescript
function flatReduce<T>(arr: T[], depth: number = 1): FlatArray<T[], number>[] {
return arr.reduce<any[]>((acc, item) => {
if (Array.isArray(item) && depth > 0) {
return acc.concat(flatReduce(item, depth - 1));
}
return acc.concat(item);
}, []);
}版本三:迭代版(栈)
避免递归可能导致的栈溢出,使用显式栈模拟递归:
typescript
function flatIterative<T>(arr: T[], depth: number = 1): FlatArray<T[], number>[] {
const stack: Array<{ value: any; depth: number }> = arr
.map((value) => ({ value, depth }))
.reverse();
const result: any[] = [];
while (stack.length > 0) {
const { value, depth: currentDepth } = stack.pop()!;
if (Array.isArray(value) && currentDepth > 0) {
for (let i = value.length - 1; i >= 0; i--) {
stack.push({ value: value[i], depth: currentDepth - 1 });
}
} else {
result.push(value);
}
}
return result;
}版本四:Generator 版
typescript
function* flatGenerator<T>(arr: T[], depth: number = 1): Generator<any> {
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
yield* flatGenerator(item, depth - 1);
} else {
yield item;
}
}
}
function flat<T>(arr: T[], depth: number = 1): FlatArray<T[], number>[] {
return [...flatGenerator(arr, depth)];
}边界情况处理
| 边界情况 | 处理方式 |
|---|---|
| 空数组 | 返回空数组 [] |
depth 为 0 | 直接返回原数组的浅拷贝 |
depth 为 Infinity | 完全扁平化 |
| 数组中有空位(sparse array) | 原生 flat 会跳过空位,实现时需注意 |
| 非数组元素 | 直接放入结果数组 |
| 深层嵌套导致栈溢出 | 使用迭代版(栈)解决 |
五、函数柯里化 curry
原理
柯里化(Currying)是将一个接受多个参数的函数转换为一系列接受单个参数的函数的技术。其数学基础来源于 λ 演算:
f(a, b, c) → f(a)(b)(c)但在实际应用中,我们通常实现的是更灵活的"偏应用"版本,允许一次传入多个参数:
f(a, b, c) 可以被调用为:
f(a)(b)(c)
f(a, b)(c)
f(a)(b, c)
f(a, b, c)核心思想:收集参数,直到参数数量足够时才执行原函数。
版本一:基础版
typescript
function curry<T extends (...args: any[]) => any>(
fn: T
): (...args: any[]) => any {
return function curried(this: any, ...args: any[]): any {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (this: any, ...moreArgs: any[]) {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}fn.length 是函数的形参个数,不包括默认参数和 rest 参数之后的参数。
版本二:支持占位符
占位符允许跳过某个参数,后续再填充:
typescript
const _ = Symbol('placeholder');
type Placeholder = typeof _;
function curryWithPlaceholder<T extends (...args: any[]) => any>(
fn: T,
placeholder: Placeholder = _
): (...args: any[]) => any {
const arity = fn.length;
return function curried(this: any, ...args: any[]): any {
const actualArgs = args.slice(0, arity);
const hasPlaceholder = actualArgs.some((arg) => arg === placeholder);
if (actualArgs.length >= arity && !hasPlaceholder) {
return fn.apply(this, actualArgs);
}
return function (this: any, ...moreArgs: any[]) {
const mergedArgs: any[] = [];
let moreIndex = 0;
for (let i = 0; i < actualArgs.length; i++) {
if (actualArgs[i] === placeholder && moreIndex < moreArgs.length) {
mergedArgs.push(moreArgs[moreIndex++]);
} else {
mergedArgs.push(actualArgs[i]);
}
}
while (moreIndex < moreArgs.length) {
mergedArgs.push(moreArgs[moreIndex++]);
}
return curried.apply(this, mergedArgs);
};
};
}使用示例:
typescript
const add = (a: number, b: number, c: number) => a + b + c;
const curriedAdd = curryWithPlaceholder(add);
curriedAdd(1, 2, 3); // 6
curriedAdd(_, 2, 3)(1); // 6 → 1 填入第一个占位符
curriedAdd(_, _, 3)(1)(2); // 6 → 1 填入第一个, 2 填入第二个
curriedAdd(_, 2)(_, 3)(1); // 6 → 先得到 f(_, 2, 3), 再用 1 填第一个边界情况处理
| 边界情况 | 处理方式 |
|---|---|
fn.length === 0 | 无参函数,直接执行 |
传入参数超过 fn.length | 多余参数被截断(占位符版)或传入原函数(基础版) |
| 函数有默认参数 | fn.length 不包含默认参数,可能提前执行 |
| rest 参数 | fn.length 不包含 rest 参数 |
this 绑定 | 使用 apply 透传 |
| 占位符填充后仍有占位符 | 继续返回新函数等待填充 |
六、EventEmitter 发布订阅
原理
发布-订阅模式是一种消息范式:发布者(Publisher)不直接向特定订阅者发送消息,而是将消息分为不同的类别(事件),订阅者(Subscriber)可以选择订阅感兴趣的事件。
┌─────────────┐ emit('click', data) ┌───────────────┐
│ Publisher │ ──────────────────────→ │ EventEmitter │
└─────────────┘ │ ┌──────────┐ │
│ │ click: │ │
┌─────────────┐ on('click', handler1) │ │ [h1, h2] │ │
│ Subscriber1 │ ←───────────────────── │ │ │ │
└─────────────┘ │ │ change: │ │
│ │ [h3] │ │
┌─────────────┐ on('click', handler2) │ └──────────┘ │
│ Subscriber2 │ ←───────────────────── └───────────────┘
└─────────────┘与观察者模式的区别:
- 观察者模式:Subject 直接通知 Observer,二者存在耦合
- 发布-订阅模式:通过 EventEmitter 作为中间层解耦,发布者和订阅者互不知情
完整实现(泛型类型安全)
typescript
type EventMap = Record<string | symbol, (...args: any[]) => void>;
interface Listener<F extends (...args: any[]) => void> {
callback: F;
once: boolean;
}
class EventEmitter<Events extends EventMap = EventMap> {
private listeners = new Map<
keyof Events,
Listener<Events[keyof Events]>[]
>();
on<K extends keyof Events>(event: K, callback: Events[K]): this {
const list = this.listeners.get(event) ?? [];
list.push({ callback, once: false });
this.listeners.set(event, list);
return this;
}
once<K extends keyof Events>(event: K, callback: Events[K]): this {
const list = this.listeners.get(event) ?? [];
list.push({ callback, once: true });
this.listeners.set(event, list);
return this;
}
off<K extends keyof Events>(event: K, callback?: Events[K]): this {
if (!callback) {
this.listeners.delete(event);
return this;
}
const list = this.listeners.get(event);
if (!list) return this;
const filtered = list.filter((listener) => listener.callback !== callback);
if (filtered.length === 0) {
this.listeners.delete(event);
} else {
this.listeners.set(event, filtered);
}
return this;
}
emit<K extends keyof Events>(
event: K,
...args: Parameters<Events[K]>
): boolean {
const list = this.listeners.get(event);
if (!list || list.length === 0) return false;
const snapshot = [...list];
const remaining: Listener<Events[keyof Events]>[] = [];
for (const listener of snapshot) {
listener.callback.apply(this, args);
if (!listener.once) {
remaining.push(listener);
}
}
if (remaining.length === 0) {
this.listeners.delete(event);
} else {
this.listeners.set(event, remaining);
}
return true;
}
listenerCount<K extends keyof Events>(event: K): number {
return this.listeners.get(event)?.length ?? 0;
}
removeAllListeners(): this {
this.listeners.clear();
return this;
}
}类型安全使用示例
typescript
interface MyEvents {
login: (userId: string, timestamp: number) => void;
logout: (userId: string) => void;
error: (error: Error) => void;
}
const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (userId, timestamp) => {
console.log(`${userId} logged in at ${timestamp}`);
});
emitter.emit('login', 'user123', Date.now());
// emitter.emit('login', 123); // TS 报错:number 不能赋值给 string
// emitter.on('unknown', () => {}); // TS 报错:'unknown' 不在 MyEvents 中边界情况处理
| 边界情况 | 处理方式 |
|---|---|
emit 时 listener 内部调用 off | 使用 snapshot(浅拷贝)遍历,避免遍历时数组变化 |
once 回调内部抛出异常 | 回调仍会被正确移除(在调用前已标记) |
| 同一个 callback 注册多次 | 每次注册都会添加,off 时全部移除匹配项 |
off 不传 callback | 移除该事件的所有监听器 |
emit 不存在的事件 | 返回 false,不抛出异常 |
| 链式调用 | on/off/once 返回 this 支持链式调用 |
七、Promise.all
原理
Promise.all 接收一个可迭代对象,返回一个新的 Promise:
- 当所有 Promise 都 fulfilled 时,返回所有结果组成的数组(保持原顺序)
- 当任一 Promise 被 rejected 时,立即返回第一个 reject 的原因(短路)
Promise.all([p1, p2, p3]):
p1: ──────✓(value1)
p2: ────────────✓(value2)
p3: ──✓(value3)
result: ────────────✓([value1, value2, value3]) ← 等最慢的完成
若 p2 失败:
p1: ──────✓(value1)
p2: ────✗(error)
p3: ──────────... ← 仍在执行但结果被忽略
result: ────✗(error) ← 立即 reject完整实现
typescript
function promiseAll<T extends readonly unknown[]>(
promises: T
): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }> {
return new Promise((resolve, reject) => {
const input = Array.from(promises as Iterable<unknown>);
const length = input.length;
if (length === 0) {
resolve([] as any);
return;
}
const results = new Array(length);
let resolvedCount = 0;
for (let i = 0; i < length; i++) {
Promise.resolve(input[i]).then(
(value) => {
results[i] = value;
resolvedCount++;
if (resolvedCount === length) {
resolve(results as any);
}
},
(reason) => {
reject(reason);
}
);
}
});
}边界情况处理
| 边界情况 | 处理方式 |
|---|---|
| 空数组 | 直接 resolve 空数组 [] |
| 包含非 Promise 值 | 使用 Promise.resolve() 包装,按值直接填入结果 |
| 结果顺序 | 使用索引 i 确保结果数组与输入顺序一致 |
| 多个 Promise reject | 只有第一个 reject 的原因会被捕获,Promise 只能 settle 一次 |
| 输入包含 thenable 对象 | Promise.resolve() 会处理任何 thenable |
八、Promise.race / Promise.allSettled / Promise.any
Promise.race
第一个 settle(无论 fulfilled 还是 rejected)的 Promise 决定最终结果:
typescript
function promiseRace<T>(
promises: Iterable<T | PromiseLike<T>>
): Promise<Awaited<T>> {
return new Promise((resolve, reject) => {
const input = Array.from(promises);
if (input.length === 0) {
return;
}
for (const item of input) {
Promise.resolve(item).then(resolve, reject);
}
});
}空数组传入 Promise.race 会返回一个永远 pending 的 Promise。
Promise.allSettled
等待所有 Promise 都 settle(无论成功还是失败),返回每个 Promise 的结果描述对象:
typescript
type SettledResult<T> =
| { status: 'fulfilled'; value: T }
| { status: 'rejected'; reason: any };
function promiseAllSettled<T>(
promises: Iterable<T | PromiseLike<T>>
): Promise<SettledResult<Awaited<T>>[]> {
return new Promise((resolve) => {
const input = Array.from(promises);
const length = input.length;
if (length === 0) {
resolve([]);
return;
}
const results: SettledResult<Awaited<T>>[] = new Array(length);
let settledCount = 0;
for (let i = 0; i < length; i++) {
Promise.resolve(input[i]).then(
(value) => {
results[i] = { status: 'fulfilled', value: value as Awaited<T> };
settledCount++;
if (settledCount === length) resolve(results);
},
(reason) => {
results[i] = { status: 'rejected', reason };
settledCount++;
if (settledCount === length) resolve(results);
}
);
}
});
}Promise.any
第一个 fulfilled 的 Promise 决定最终结果。如果所有 Promise 都 rejected,则返回 AggregateError:
typescript
function promiseAny<T>(
promises: Iterable<T | PromiseLike<T>>
): Promise<Awaited<T>> {
return new Promise((resolve, reject) => {
const input = Array.from(promises);
const length = input.length;
if (length === 0) {
reject(new AggregateError([], 'All promises were rejected'));
return;
}
const errors: any[] = new Array(length);
let rejectedCount = 0;
for (let i = 0; i < length; i++) {
Promise.resolve(input[i]).then(
(value) => {
resolve(value as Awaited<T>);
},
(reason) => {
errors[i] = reason;
rejectedCount++;
if (rejectedCount === length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
}
});
}四者对比
│ 短路条件 │ 空数组行为 │ 返回值
─────────────┼──────────────┼──────────────────┼─────────────────
Promise.all │ 任一 reject │ resolve([]) │ 所有成功值的数组
Promise.race │ 任一 settle │ 永远 pending │ 第一个 settle 的值
allSettled │ 不短路 │ resolve([]) │ 所有结果描述对象
Promise.any │ 任一 fulfill │ AggregateError │ 第一个成功值九、手写 call / apply / bind
原理
this 的指向在 JavaScript 中由调用方式决定,而非定义位置(箭头函数除外)。call/apply/bind 提供了显式绑定 this 的能力。
核心原理:将函数临时挂载到目标对象上,通过 obj.fn() 的方式调用,利用隐式绑定使 this 指向 obj。
fn.call(obj, arg1, arg2)
等价于:
obj.__temp__ = fn
obj.__temp__(arg1, arg2)
delete obj.__temp__实现 call
typescript
function myCall<T, A extends any[], R>(
fn: (this: T, ...args: A) => R,
thisArg: T,
...args: A
): R {
const context = thisArg === null || thisArg === undefined
? globalThis
: Object(thisArg);
const key = Symbol();
context[key] = fn;
const result = context[key](...args);
delete context[key];
return result;
}使用 Symbol() 作为临时属性名,避免与对象已有属性冲突。
实现 apply
typescript
function myApply<T, A extends any[], R>(
fn: (this: T, ...args: A) => R,
thisArg: T,
args?: A
): R {
const context = thisArg === null || thisArg === undefined
? globalThis
: Object(thisArg);
const key = Symbol();
context[key] = fn;
const result = args ? context[key](...args) : context[key]();
delete context[key];
return result;
}实现 bind
bind 比 call/apply 更复杂,因为它返回一个新函数,并且需要处理 new 调用的情况:
typescript
function myBind<T, A extends any[], R>(
fn: (this: T, ...args: A) => R,
thisArg: T,
...partialArgs: any[]
): (...args: any[]) => R {
const boundFn = function (this: any, ...args: any[]): R {
const finalArgs = [...partialArgs, ...args];
const isNewCall = this instanceof boundFn;
return fn.apply(
isNewCall ? this : thisArg,
finalArgs as unknown as A
);
};
if (fn.prototype) {
boundFn.prototype = Object.create(fn.prototype);
}
return boundFn;
}边界情况处理
| 边界情况 | 处理方式 |
|---|---|
thisArg 为 null 或 undefined | 非严格模式下指向 globalThis |
thisArg 为原始值 | 使用 Object() 包装为对象(装箱) |
apply 的 args 为 undefined | 不传参调用 |
bind 后用 new 调用 | this 绑定到新创建的对象,忽略 thisArg |
bind 后的原型链 | 需要让 boundFn.prototype 继承 fn.prototype |
| 属性名冲突 | 使用 Symbol 作为临时属性 key |
十、instanceof 实现
原理
instanceof 运算符检查构造函数的 prototype 属性是否出现在对象的原型链上。
obj instanceof Constructor
等价于遍历原型链:
obj.__proto__ → Constructor.prototype ? ✓ true
obj.__proto__.__proto__ → Constructor.prototype ? ✓ true
...
null → 到达原型链顶端 ✗ false完整实现
typescript
function myInstanceof(obj: unknown, constructor: Function): boolean {
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
return false;
}
if (typeof constructor !== 'function') {
throw new TypeError('Right-hand side of instanceof is not callable');
}
const prototype = constructor.prototype;
if (prototype === null || typeof prototype !== 'object') {
throw new TypeError('Function has non-object prototype in instanceof check');
}
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}边界情况处理
| 边界情况 | 处理方式 |
|---|---|
| 原始类型 | 直接返回 false(原始类型没有原型链) |
null | 直接返回 false |
| 右侧不是函数 | 抛出 TypeError |
constructor.prototype 为 null | 抛出 TypeError |
Object.create(null) | 原型链为空,返回 false |
十一、new 操作符实现
原理
new 操作符执行以下步骤:
new Constructor(args):
1. 创建一个新的空对象 const obj = {}
2. 将新对象的原型指向构造函数的 prototype obj.__proto__ = Constructor.prototype
3. 执行构造函数,this 绑定到新对象 const result = Constructor.apply(obj, args)
4. 如果构造函数返回了对象,则使用该返回值 return isObject(result) ? result : obj
否则返回新创建的对象完整实现
typescript
function myNew<T extends new (...args: any[]) => any>(
Constructor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
if (typeof Constructor !== 'function') {
throw new TypeError(`${String(Constructor)} is not a constructor`);
}
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
if (result !== null && (typeof result === 'object' || typeof result === 'function')) {
return result;
}
return obj;
}第 4 步返回值处理详解
typescript
function Foo() {
this.name = 'foo';
return { custom: true };
}
const a = new Foo();
console.log(a); // { custom: true } — 构造函数返回了对象,使用该返回值
function Bar() {
this.name = 'bar';
return 42;
}
const b = new Bar();
console.log(b); // { name: 'bar' } — 返回原始值被忽略,使用新创建的对象边界情况处理
| 边界情况 | 处理方式 |
|---|---|
| 构造函数返回对象 | 使用返回的对象而非新创建的对象 |
| 构造函数返回原始值 | 忽略返回值,使用新创建的对象 |
构造函数返回 null | null 虽然 typeof 是 object,但按规范忽略,使用新对象 |
| 构造函数返回函数 | 函数也是对象,使用返回的函数 |
| 非函数传入 | 抛出 TypeError |
| 箭头函数 | 箭头函数没有 prototype,不能作为构造函数 |
十二、JSON.stringify / JSON.parse 简易实现
JSON.stringify 实现
需要处理各种 JavaScript 类型到 JSON 字符串的转换:
typescript
function jsonStringify(value: unknown): string | undefined {
if (value === null) return 'null';
if (value === undefined) return undefined;
if (typeof value === 'boolean') return String(value);
if (typeof value === 'number') {
if (Number.isNaN(value) || !Number.isFinite(value)) return 'null';
return String(value);
}
if (typeof value === 'string') {
return `"${value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')}"`;
}
if (typeof value === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt');
}
if (typeof value === 'symbol' || typeof value === 'function') {
return undefined;
}
if (typeof value === 'object') {
if (typeof (value as any).toJSON === 'function') {
return jsonStringify((value as any).toJSON());
}
if (Array.isArray(value)) {
const items = value.map((item) => {
const result = jsonStringify(item);
return result === undefined ? 'null' : result;
});
return `[${items.join(',')}]`;
}
const entries: string[] = [];
for (const key of Object.keys(value)) {
const val = jsonStringify((value as Record<string, unknown>)[key]);
if (val !== undefined) {
entries.push(`${jsonStringify(key)}:${val}`);
}
}
return `{${entries.join(',')}}`;
}
return undefined;
}JSON.parse 简易实现
使用递归下降解析器实现:
typescript
function jsonParse(json: string): unknown {
let index = 0;
function parseValue(): unknown {
skipWhitespace();
const char = json[index];
if (char === '"') return parseString();
if (char === '{') return parseObject();
if (char === '[') return parseArray();
if (char === 't') return parseLiteral('true', true);
if (char === 'f') return parseLiteral('false', false);
if (char === 'n') return parseLiteral('null', null);
if (char === '-' || (char >= '0' && char <= '9')) return parseNumber();
throw new SyntaxError(`Unexpected token ${char} at position ${index}`);
}
function skipWhitespace() {
while (index < json.length && ' \t\n\r'.includes(json[index])) {
index++;
}
}
function parseString(): string {
index++;
let result = '';
while (index < json.length) {
const char = json[index];
if (char === '"') {
index++;
return result;
}
if (char === '\\') {
index++;
const escaped = json[index];
const escapeMap: Record<string, string> = {
'"': '"', '\\': '\\', '/': '/', b: '\b',
f: '\f', n: '\n', r: '\r', t: '\t',
};
if (escaped in escapeMap) {
result += escapeMap[escaped];
} else if (escaped === 'u') {
const hex = json.slice(index + 1, index + 5);
result += String.fromCharCode(parseInt(hex, 16));
index += 4;
}
} else {
result += char;
}
index++;
}
throw new SyntaxError('Unterminated string');
}
function parseNumber(): number {
const start = index;
if (json[index] === '-') index++;
if (json[index] === '0') {
index++;
} else {
while (index < json.length && json[index] >= '0' && json[index] <= '9') {
index++;
}
}
if (json[index] === '.') {
index++;
while (index < json.length && json[index] >= '0' && json[index] <= '9') {
index++;
}
}
if (json[index] === 'e' || json[index] === 'E') {
index++;
if (json[index] === '+' || json[index] === '-') index++;
while (index < json.length && json[index] >= '0' && json[index] <= '9') {
index++;
}
}
return Number(json.slice(start, index));
}
function parseObject(): Record<string, unknown> {
index++;
const obj: Record<string, unknown> = {};
skipWhitespace();
if (json[index] === '}') {
index++;
return obj;
}
while (index < json.length) {
skipWhitespace();
const key = parseString();
skipWhitespace();
if (json[index] !== ':') {
throw new SyntaxError(`Expected ':' at position ${index}`);
}
index++;
obj[key] = parseValue();
skipWhitespace();
if (json[index] === '}') {
index++;
return obj;
}
if (json[index] !== ',') {
throw new SyntaxError(`Expected ',' at position ${index}`);
}
index++;
}
throw new SyntaxError('Unterminated object');
}
function parseArray(): unknown[] {
index++;
const arr: unknown[] = [];
skipWhitespace();
if (json[index] === ']') {
index++;
return arr;
}
while (index < json.length) {
arr.push(parseValue());
skipWhitespace();
if (json[index] === ']') {
index++;
return arr;
}
if (json[index] !== ',') {
throw new SyntaxError(`Expected ',' at position ${index}`);
}
index++;
}
throw new SyntaxError('Unterminated array');
}
function parseLiteral<T>(literal: string, value: T): T {
if (json.slice(index, index + literal.length) === literal) {
index += literal.length;
return value;
}
throw new SyntaxError(`Unexpected token at position ${index}`);
}
const result = parseValue();
skipWhitespace();
if (index < json.length) {
throw new SyntaxError(`Unexpected token at position ${index}`);
}
return result;
}边界情况处理
| 边界情况 | JSON.stringify 处理 | JSON.parse 处理 |
|---|---|---|
undefined / Function / Symbol | 对象属性中被忽略,数组中变成 null | 不存在此类输入 |
NaN / Infinity | 转为 null | 不是合法 JSON |
BigInt | 抛出 TypeError | 不是合法 JSON |
| 循环引用 | 需要检测并抛出错误(简化版未处理) | 不存在此类输入 |
toJSON 方法 | 优先调用 toJSON() 的返回值 | N/A |
| 转义字符 | 处理 " \ \n \r \t 等 | 处理 \" \\ \n \uXXXX 等 |
| 空字符串输入 | N/A | 抛出 SyntaxError |
| 嵌套过深 | 可能栈溢出 | 可能栈溢出 |
面试题(5 道)
题目一:实现一个带最大并发限制的异步调度器
题目:实现一个 Scheduler 类,它接受一个最大并发数 maxConcurrency,提供 add(task) 方法添加异步任务。同一时刻最多只有 maxConcurrency 个任务在执行。
解答:
typescript
class Scheduler {
private maxConcurrency: number;
private runningCount = 0;
private queue: Array<() => void> = [];
constructor(maxConcurrency: number) {
this.maxConcurrency = maxConcurrency;
}
add<T>(task: () => Promise<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
const wrappedTask = () => {
this.runningCount++;
task().then(resolve, reject).finally(() => {
this.runningCount--;
this.tryRunNext();
});
};
if (this.runningCount < this.maxConcurrency) {
wrappedTask();
} else {
this.queue.push(wrappedTask);
}
});
}
private tryRunNext() {
if (this.queue.length > 0 && this.runningCount < this.maxConcurrency) {
const next = this.queue.shift()!;
next();
}
}
}验证:
typescript
const scheduler = new Scheduler(2);
const delay = (ms: number, label: string) =>
scheduler.add(
() => new Promise<void>((resolve) => {
console.log(`${label} start`);
setTimeout(() => {
console.log(`${label} end`);
resolve();
}, ms);
})
);
delay(1000, 'A');
delay(500, 'B');
delay(300, 'C');
delay(400, 'D');输出顺序:A start → B start → B end → C start → A end → D start → C end → D end。解释:A 和 B 首先开始(并发 2),B 500ms 后完成,C 开始;A 1000ms 后完成,D 开始。
题目二:实现防抖 debounce 的 React Hook
题目:实现 useDebounce Hook,要求组件卸载时自动取消,并且在依赖变化时正确更新。
解答:
typescript
import { useCallback, useEffect, useRef } from 'react';
function useDebounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
const fnRef = useRef(fn);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
fnRef.current = fn;
}, [fn]);
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
return useCallback(
(...args: Parameters<T>) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
fnRef.current(...args);
timerRef.current = null;
}, delay);
},
[delay]
);
}关键设计决策解析:
useRef存储最新 fn:避免将fn放入useCallback的依赖数组,否则每次fn变化都会创建新的防抖函数,导致之前的计时器被遗忘- 卸载时清理:
useEffect的清理函数确保组件卸载时取消未执行的定时器 delay作为依赖:delay变化时重新创建防抖函数是合理的
题目三:实现深拷贝处理特殊对象
题目:以下代码使用 JSON.parse(JSON.stringify()) 进行深拷贝,指出所有问题并给出修复方案。
typescript
const original = {
date: new Date('2024-01-01'),
pattern: /\d+/g,
data: new Map([['key', { nested: true }]]),
ref: null as any,
};
original.ref = original;
const clone = JSON.parse(JSON.stringify(original));解答:
问题分析:
Date→ 变成字符串"2024-01-01T00:00:00.000Z",类型从 Date 变成 stringRegExp→ 变成空对象{},丢失正则表达式内容Map→ 变成空对象{},所有键值对丢失- 循环引用 → 直接抛出
TypeError: Converting circular structure to JSON
修复方案使用之前实现的 deepClone:
typescript
const clone = deepClone(original);
console.log(clone.date instanceof Date); // true
console.log(clone.pattern instanceof RegExp); // true
console.log(clone.data instanceof Map); // true
console.log(clone.ref === clone); // true(正确的循环引用)
console.log(clone.data.get('key').nested); // true
console.log(clone !== original); // true(不同对象)也可以使用 structuredClone,但需注意它会丢失 RegExp 的 lastIndex 属性和原型链信息。
题目四:用 Promise 实现红绿灯交替
题目:红灯亮 3 秒,绿灯亮 2 秒,黄灯亮 1 秒,三者交替循环,使用 Promise 实现。
解答:
typescript
function light(color: string, duration: number): Promise<void> {
return new Promise((resolve) => {
console.log(`${color} light on`);
setTimeout(resolve, duration);
});
}
async function trafficLight(): Promise<never> {
while (true) {
await light('red', 3000);
await light('green', 2000);
await light('yellow', 1000);
}
}
trafficLight();这道题的核心考察点:
- Promise 的串行执行:
await确保每个灯亮完才切换到下一个 - 无限循环:
while(true)+async/await实现非阻塞的无限循环 - 返回类型
never:函数永远不会正常返回
扩展:如果要支持取消功能:
typescript
function createTrafficLight(): { start: () => void; stop: () => void } {
let running = false;
let timer: ReturnType<typeof setTimeout> | null = null;
function delay(ms: number): Promise<void> {
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
if (running) resolve();
else reject(new Error('stopped'));
}, ms);
});
}
async function run() {
running = true;
try {
while (running) {
console.log('red');
await delay(3000);
console.log('green');
await delay(2000);
console.log('yellow');
await delay(1000);
}
} catch {}
}
return {
start: run,
stop: () => {
running = false;
if (timer) clearTimeout(timer);
},
};
}题目五:实现 LazyMan
题目:实现一个 LazyMan,支持链式调用,按照调用顺序输出。
typescript
LazyMan('Jack') // Hi! I'm Jack.
.sleep(2) // (等待2秒) Wake up after 2s
.eat('lunch') // Eat lunch
.sleepFirst(1) // (等待1秒,但优先执行) Wake up after 1s最终输出顺序:Wake up after 1s → Hi! I'm Jack. → Wake up after 2s → Eat lunch
解答:
typescript
class LazyManImpl {
private queue: Array<() => Promise<void>> = [];
constructor(name: string) {
this.queue.push(async () => {
console.log(`Hi! I'm ${name}.`);
});
queueMicrotask(() => this.run());
}
private async run() {
for (const task of this.queue) {
await task();
}
}
eat(food: string): this {
this.queue.push(async () => {
console.log(`Eat ${food}`);
});
return this;
}
sleep(seconds: number): this {
this.queue.push(
() =>
new Promise((resolve) => {
setTimeout(() => {
console.log(`Wake up after ${seconds}s`);
resolve();
}, seconds * 1000);
})
);
return this;
}
sleepFirst(seconds: number): this {
this.queue.unshift(
() =>
new Promise((resolve) => {
setTimeout(() => {
console.log(`Wake up after ${seconds}s`);
resolve();
}, seconds * 1000);
})
);
return this;
}
}
function LazyMan(name: string): LazyManImpl {
return new LazyManImpl(name);
}核心设计解析:
queueMicrotask:在当前同步代码执行完毕后开始执行任务队列。这样所有的链式调用(同步代码)都会先完成,任务都被收集到队列中sleepFirst用unshift:插入到队列头部,确保优先执行async/await串行执行:保证任务按顺序执行,sleep会真正等待
追问思考(5 道)
防抖和节流的底层都依赖
setTimeout,而setTimeout的最小延迟是 4ms(HTML 规范),这在什么场景下会成为问题?如果需要更精确的时间控制,有什么替代方案(requestAnimationFrame、requestIdleCallback、MessageChannel)?各自的适用场景是什么?深拷贝中使用
WeakMap处理循环引用,但WeakMap的 key 只能是对象。如果需要实现一个支持SharedArrayBuffer、DataView、Error对象、以及自定义 class 实例(包含私有字段#field)的深拷贝,应该如何扩展?私有字段为什么无法从外部拷贝,有什么变通方案?柯里化在 TypeScript 中的类型推导是一个经典难题:如何让
curry(fn)返回的函数在每次传入部分参数后,仍然拥有正确的剩余参数类型提示?请思考如何使用 TypeScript 的条件类型、递归类型和元组操作来实现精确的柯里化类型签名。EventEmitter在大型应用中可能出现内存泄漏(忘记off)。Node.js 的 EventEmitter 默认对同一事件超过 10 个 listener 会发出警告。如何设计一个更安全的 EventEmitter,包含:最大监听器数量限制、自动弱引用清理(WeakRef + FinalizationRegistry)、以及基于 AbortController 的批量取消能力?Promise.all在实际项目中有一个常见的变体需求:带并发限制的Promise.all(比如同时最多发 5 个 HTTP 请求)。请思考如何基于本文的Scheduler实现一个promiseAllWithLimit<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]>,并且要求:保持结果顺序、支持错误短路、支持通过 AbortSignal 取消所有未完成的任务。