外观
常见手写题
- 状态:未准备
- 适用方向:前端 / Go 全栈
- 最近复习:待填写
- 下次复习:待安排
一句话回答
手写题先声明支持范围与边界,再写核心逻辑,用可观察的输入输出证明行为;面试简化实现应明确区别于原生 API 和生产库。
原理解释
1. 防抖:尾沿执行,支持取消
js
function debounce(fn, wait) {
let timer
function wrapped(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
timer = undefined
fn.apply(this, args)
}, wait)
}
wrapped.cancel = () => {
clearTimeout(timer)
timer = undefined
}
return wrapped
}连续触发时重新计时,停止 wait 后只执行最后一次;适合搜索输入。保留最后调用的 this 和参数,组件卸载时 cancel。此版无 leading、flush、maxWait,也不向调用者返回异步执行结果;持续输入可能一直不执行。
2. 节流:首沿执行,无尾沿补调
js
function throttle(fn, wait) {
let last = -Infinity
return function (...args) {
const now = performance.now()
if (now - last >= wait) {
last = now
return fn.apply(this, args)
}
}
}一定时间窗口最多执行一次,适合允许丢弃中间事件的场景。此版不会补执行窗口末尾事件,不适合要求保留最后值的需求。performance.now 是单调计时,避免系统时间调整影响间隔计算。
3. 深拷贝:只支持普通对象、数组和原始值
js
function clonePlain(value, seen = new WeakMap()) {
if (typeof value === 'function') throw new TypeError('Unsupported function')
if (value === null || typeof value !== 'object') return value
if (seen.has(value)) return seen.get(value)
const proto = Object.getPrototypeOf(value)
if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) {
throw new TypeError('Only plain objects and arrays are supported')
}
const copy = Array.isArray(value) ? new Array(value.length) : Object.create(proto)
seen.set(value, copy)
for (const key of Reflect.ownKeys(value)) {
if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue
Object.defineProperty(copy, key, {
value: clonePlain(value[key], seen),
enumerable: true, writable: true, configurable: true,
})
}
return copy
}先登记映射再递归,处理循环和共享引用;defineProperty 避免把 __proto__ 当成原型赋值。这里只复制自身可枚举属性,访问器会被读取,不保留描述符;不支持 Map、Set、Date、DOM 等。生产中先考虑 structuredClone 或经过验证的方案。
4. 并发池:限制同时运行的任务数
js
async function mapLimit(items, limit, task) {
if (!Number.isInteger(limit) || limit < 1) {
throw new RangeError('limit must be a positive integer')
}
const results = new Array(items.length)
let next = 0
let stopped = false
async function worker() {
while (!stopped && next < items.length) {
const index = next++
try {
results[index] = await task(items[index], index)
} catch (error) {
stopped = true
throw error
}
}
}
await Promise.all(Array.from(
{ length: Math.min(limit, items.length) }, worker,
))
return results
}任务应通过函数延迟启动,而非传入一组已启动的 Promise。结果按输入顺序保存;首个错误使外层拒绝,并阻止发现错误后启动更多任务,但已执行的任务不会自动取消。空数组返回空数组;此版假设输入是有限且不在执行期间修改的普通稠密数组。
5. 简化 new:只覆盖普通函数构造器
js
function myNew(Ctor, ...args) {
// 约定 Ctor 是可用 apply 调用的普通构造函数。
if (typeof Ctor !== 'function' || !Ctor.prototype) {
throw new TypeError('Unsupported constructor')
}
const instance = Object.create(Ctor.prototype)
const result = Ctor.apply(instance, args)
return result !== null &&
(typeof result === 'object' || typeof result === 'function')
? result : instance
}不支持 class、new.target、绑定构造函数和需要内建槽位的构造器等完整语义;完整构造可使用 Reflect.construct。面试中先解释简化范围,不把此实现描述为原生 new 的完全替代。
场景与边界
| 题目 | 至少验证的行为 | 继续追问 |
|---|---|---|
| 防抖 | 连续触发只保留最后参数,取消后不执行,this 正确 | 首尾沿、最大等待、返回值 |
| 节流 | 首次立即执行,窗口内抑制,窗口后允许 | 是否保留尾沿、系统时间 |
| 深拷贝 | 嵌套对象独立、循环、自共享、稀疏数组、Symbol 键 | 原型、访问器、特殊类型 |
| 并发池 | 峰值不超过 limit、保持输入顺序、空数组、拒绝 | 取消、重试、allSettled 模式 |
| new | 实例属性、原型关系、返回对象/原始值 | class、Reflect.construct |
项目中的应用
结合 Blog 滚动调度 比较 rAF 与防抖/节流;结合 SimulSpeak 字幕 解释节流的最后值是否允许丢弃。学习代码不代表项目实际采用这些版本。
常见追问
- Promise.all 等于并发池吗? 不等于,all 聚合结果,本身不限制已启动任务的数量。
- JSON 拷贝为什么不够? 类型和引用图语义会丢失,循环引用会失败。
- 手写 bind 要注意什么? 普通调用需处理 this 和预置参数,构造调用还要处理原型和构造语义;简化版必须先声明不支持构造。