技术面试复盘:高频算法题的前端实现思路(防抖、节流、深拷贝等)
复盘目标
- 以可复用的前端实现为主线,覆盖概念、边界与代码
- 题目聚焦高频场景:防抖、节流、深拷贝、深比较、并发控制、柯里化与组合、Promise 工具、事件总线
防抖(Debounce)
type DebounceOptions = { leading?: boolean; trailing?: boolean; maxWait?: number }
export function debounce<T extends (...args: any[]) => any>(fn: T, wait = 100, options: DebounceOptions = {}) {
let timer: any = null
let lastArgs: any[] | null = null
let lastThis: any
let lastCallTime = 0
let lastInvokeTime = 0
const leading = options.leading === true
const trailing = options.trailing !== false
const maxWait = typeof options.maxWait === 'number' ? options.maxWait : 0
function invoke(time: number) {
const args = lastArgs
const context = lastThis
lastArgs = null
lastThis = null
lastInvokeTime = time
return fn.apply(context, args as any)
}
function startTimer(pending: () => void, ms: number) {
timer = setTimeout(pending, ms)
}
function remainingWait(time: number) {
const sinceLastCall = time - lastCallTime
const sinceLastInvoke = time - lastInvokeTime
const waitTime = wait - sinceLastCall
return maxWait ? Math.min(waitTime, maxWait - sinceLastInvoke) : waitTime
}
function shouldInvoke(time: number) {
const sinceLastCall = time - lastCallTime
const sinceLastInvoke = time - lastInvokeTime
return lastCallTime === 0 || sinceLastCall >= wait || sinceLastCall < 0 || (maxWait && sinceLastInvoke >= maxWait)
}
function trailingInvoke(time: number) {
if (trailing && lastArgs) return invoke(time)
lastArgs = null
lastThis = null
return undefined
}
function debounced(this: any, ...args: any[]) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (!timer) {
if (leading) invoke(time)
startTimer(timerExpired, remainingWait(time))
} else if (maxWait) {
startTimer(timerExpired, remainingWait(time))
}
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
timer = null
trailingInvoke(time)
} else {
startTimer(timerExpired, remainingWait(time))
}
}
;(debounced as any).cancel = () => {
if (timer) clearTimeout(timer)
timer = null
lastArgs = null
lastThis = null
lastCallTime = 0
lastInvokeTime = 0
}
;(debounced as any).flush = () => {
if (timer) {
clearTimeout(timer)
timer = null
return trailingInvoke(Date.now())
}
}
return debounced as T & { cancel: () => void; flush: () => any }
}
节流(Throttle)
type ThrottleOptions = { leading?: boolean; trailing?: boolean }
export function throttle<T extends (...args: any[]) => any>(fn: T, wait = 100, options: ThrottleOptions = {}) {
let timer: any = null
let lastArgs: any[] | null = null
let lastThis: any
let lastInvoke = 0
const leading = options.leading !== false
const trailing = options.trailing !== false
function invoke(time: number) {
lastInvoke = time
const res = fn.apply(lastThis, lastArgs as any)
lastArgs = null
lastThis = null
return res
}
function trailingInvoke() {
if (trailing && lastArgs) invoke(Date.now())
}
function throttled(this: any, ...args: any[]) {
const time = Date.now()
if (!lastInvoke && !leading) lastInvoke = time
const remaining = wait - (time - lastInvoke)
lastArgs = args
lastThis = this
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer)
timer = null
}
invoke(time)
} else if (!timer && trailing) {
timer = setTimeout(() => {
timer = null
trailingInvoke()
}, remaining)
}
}
;(throttled as any).cancel = () => {
if (timer) clearTimeout(timer)
timer = null
lastArgs = null
lastThis = null
lastInvoke = 0
}
return throttled as T & { cancel: () => void }
}
深拷贝(Deep Clone)
export function deepClone<T>(input: T, cache = new WeakMap()): T {
if (typeof input !== 'object' || input === null) return input
if (cache.has(input as any)) return cache.get(input as any)
if (input instanceof Date) return new Date(input.getTime()) as any
if (input instanceof RegExp) return new RegExp(input.source, input.flags) as any
if (input instanceof Map) {
const m = new Map()
cache.set(input as any, m as any)
for (const [k, v] of input as any as Map<any, any>) m.set(deepClone(k, cache), deepClone(v, cache))
return m as any
}
if (input instanceof Set) {
const s = new Set()
cache.set(input as any, s as any)
for (const v of input as any as Set<any>) s.add(deepClone(v, cache))
return s as any
}
if (ArrayBuffer.isView(input)) {
const Ctor: any = (input as any).constructor
return new Ctor((input as any))
}
const isArray = Array.isArray(input)
const proto = Object.getPrototypeOf(input as any)
const result: any = isArray ? [] : Object.create(proto)
cache.set(input as any, result)
for (const key of Reflect.ownKeys(input as any)) {
const desc = Object.getOwnPropertyDescriptor(input as any, key)!
if (desc.get || desc.set) Object.defineProperty(result, key, desc)
else result[key as any] = deepClone((input as any)[key as any], cache)
}
return result
}
深比较(Deep Equal)
export function deepEqual(a: any, b: any, seen = new WeakMap()): boolean {
if (Object.is(a, b)) return true
if (typeof a !== typeof b) return false
if (typeof a !== 'object' || a === null || b === null) return false
if (seen.get(a) === b) return true
seen.set(a, b)
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false
for (const [k, v] of a) if (!b.has(k) || !deepEqual(v, b.get(k), seen)) return false
return true
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false
for (const v of a) if (![...b].some(x => deepEqual(v, x, seen))) return false
return true
}
const keysA = Reflect.ownKeys(a)
const keysB = Reflect.ownKeys(b)
if (keysA.length !== keysB.length) return false
for (const k of keysA) {
const da = Object.getOwnPropertyDescriptor(a, k)
const db = Object.getOwnPropertyDescriptor(b, k)
if (!!da?.get !== !!db?.get || !!da?.set !== !!db?.set) return false
if (da && !da.get && !da.set) if (!deepEqual((a as any)[k as any], (b as any)[k as any], seen)) return false
}
return true
}
并发控制(限制同时执行的任务数)
export function createScheduler(limit = 5) {
let active = 0
const queue: Array<{ task: () => Promise<any>; resolve: (v: any) => void; reject: (e: any) => void }> = []
function run() {
while (active < limit && queue.length) {
const item = queue.shift()!
active++
Promise.resolve().then(item.task).then(v => { active--; item.resolve(v); run() }).catch(e => { active--; item.reject(e); run() })
}
}
return function add(task: () => Promise<any>) {
return new Promise((resolve, reject) => { queue.push({ task, resolve, reject }); run() })
}
}
柯里化与组合
export function curry(fn: Function, arity = fn.length) {
function curried(this: any, ...args: any[]) {
if (args.length >= arity) return fn.apply(this, args)
return (...rest: any[]) => curried.apply(this, args.concat(rest))
}
return curried
}
export const compose = (...fns: Function[]) => (x: any) => fns.reduceRight((v, f) => f(v), x)
Promise 工具
export function timeout<T>(p: Promise<T>, ms = 2000) {
return Promise.race([p, new Promise<T>((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))])
}
export async function retry<T>(fn: () => Promise<T>, times = 3, delay = 200, factor = 2) {
let d = delay
for (let i = 0; i < times; i++) {
try { return await fn() } catch { if (i === times - 1) throw new Error('retry failed'); await new Promise(r => setTimeout(r, d)); d *= factor }
}
}
export function allSettled<T>(arr: Iterable<T | Promise<T>>) {
return Promise.all(Array.from(arr, p => Promise.resolve(p).then(v => ({ status: 'fulfilled', value: v })).catch(e => ({ status: 'rejected', reason: e }))))
}
事件总线(EventEmitter)
export class Emitter {
private store = new Map<string, Set<Function>>()
on(event: string, handler: Function) { if (!this.store.has(event)) this.store.set(event, new Set()); this.store.get(event)!.add(handler); return () => this.off(event, handler) }
once(event: string, handler: Function) { const wrap = (...args: any[]) => { this.off(event, wrap); handler(...args) }; return this.on(event, wrap) }
off(event: string, handler: Function) { const set = this.store.get(event); if (set) set.delete(handler) }
emit(event: string, ...args: any[]) { const set = this.store.get(event); if (!set) return; for (const h of set) h(...args) }
}
使用与验证要点
- 防抖与节流需根据交互选择领先/尾随与最大等待
- 深拷贝与深比较需覆盖 Map/Set/Date/RegExp/TypedArray 与循环引用
- 并发控制适用于批量请求或上传,避免阻塞与雪崩
- Promise 工具与事件总线是常见基础设施,便于题目延伸