跳到正文

常见手写题 ​

  • 状态:未准备
  • 适用方向:前端 / 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 和预置参数,构造调用还要处理原型和构造语义;简化版必须先声明不支持构造。

参考资料 ​

理解机制,讲清取舍,用复盘检验掌握。