Skip to content

前端手写题集 ​

面试让你「手写一个 debounce」的时候,考的是边界:能不能处理 this、参数、立即执行、返回值和取消。 这篇 20+ 段,每段都是可以直接手敲出来的版本,并且标了最容易错的那一行。

一、防抖与节流 ​

1. 防抖 debounce(含立即执行与取消) ​

js
function debounce(fn, wait = 300, immediate = false) {
  let timer = null
  let result

  function debounced(...args) {
    if (timer) clearTimeout(timer)

    if (immediate) {
      // 立即执行版:没有定时器说明是「新一轮」
      const callNow = !timer
      timer = setTimeout(() => { timer = null }, wait)
      if (callNow) result = fn.apply(this, args)
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args)   // 关键:保留 this 和参数
        timer = null
      }, wait)
    }
    return result
  }

  debounced.cancel = () => {
    clearTimeout(timer)
    timer = null
  }
  return debounced
}
function debounce(fn, wait = 300, immediate = false) {
  let timer = null
  let result

  function debounced(...args) {
    if (timer) clearTimeout(timer)

    if (immediate) {
      // 立即执行版:没有定时器说明是「新一轮」
      const callNow = !timer
      timer = setTimeout(() => { timer = null }, wait)
      if (callNow) result = fn.apply(this, args)
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args)   // 关键:保留 this 和参数
        timer = null
      }, wait)
    }
    return result
  }

  debounced.cancel = () => {
    clearTimeout(timer)
    timer = null
  }
  return debounced
}

易错点:fn.apply(this, args) —— 直接写 fn() 会丢 this;cancel 要顺手实现,面试官常追问。

2. 节流 throttle(时间戳 + 定时器,两种都要会) ​

js
// ① 时间戳版:第一次立即执行,最后一次可能丢
function throttle(fn, wait = 300) {
  let last = 0
  return function (...args) {
    const now = Date.now()
    if (now - last >= wait) {
      last = now
      fn.apply(this, args)
    }
  }
}

// ② 定时器版:第一次不立即执行,最后一次一定执行
function throttle2(fn, wait = 300) {
  let timer = null
  return function (...args) {
    if (timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, wait)
  }
}

// ③ 结合版:既立即执行,又保证最后一次
function throttle3(fn, wait = 300) {
  let last = 0, timer = null
  return function (...args) {
    const now = Date.now()
    const remain = wait - (now - last)
    if (remain <= 0) {
      last = now
      fn.apply(this, args)
    } else if (!timer) {
      timer = setTimeout(() => {
        last = Date.now()
        timer = null
        fn.apply(this, args)
      }, remain)
    }
  }
}
// ① 时间戳版:第一次立即执行,最后一次可能丢
function throttle(fn, wait = 300) {
  let last = 0
  return function (...args) {
    const now = Date.now()
    if (now - last >= wait) {
      last = now
      fn.apply(this, args)
    }
  }
}

// ② 定时器版:第一次不立即执行,最后一次一定执行
function throttle2(fn, wait = 300) {
  let timer = null
  return function (...args) {
    if (timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, wait)
  }
}

// ③ 结合版:既立即执行,又保证最后一次
function throttle3(fn, wait = 300) {
  let last = 0, timer = null
  return function (...args) {
    const now = Date.now()
    const remain = wait - (now - last)
    if (remain <= 0) {
      last = now
      fn.apply(this, args)
    } else if (!timer) {
      timer = setTimeout(() => {
        last = Date.now()
        timer = null
        fn.apply(this, args)
      }, remain)
    }
  }
}
防抖节流
触发时机停止操作 wait 后才执行每 wait 至少执行一次
场景搜索输入、窗口 resize、表单校验滚动加载、拖拽、按钮防连点

二、深拷贝 ​

js
function deepClone(target, map = new WeakMap()) {
  // 基本类型与函数直接返回
  if (target === null || typeof target !== 'object') return target

  // 处理循环引用
  if (map.has(target)) return map.get(target)

  let clone
  // 特殊对象:Date / RegExp / Map / Set
  if (target instanceof Date) return new Date(target)
  if (target instanceof RegExp) return new RegExp(target.source, target.flags)
  if (target instanceof Map) {
    clone = new Map()
    map.set(target, clone)
    target.forEach((v, k) => clone.set(deepClone(k, map), deepClone(v, map)))
    return clone
  }
  if (target instanceof Set) {
    clone = new Set()
    map.set(target, clone)
    target.forEach(v => clone.add(deepClone(v, map)))
    return clone
  }

  // 数组和普通对象
  clone = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target))
  map.set(target, clone)

  for (const key of Reflect.ownKeys(target)) {      // 含 Symbol、不可枚举
    clone[key] = deepClone(target[key], map)
  }
  return clone
}
function deepClone(target, map = new WeakMap()) {
  // 基本类型与函数直接返回
  if (target === null || typeof target !== 'object') return target

  // 处理循环引用
  if (map.has(target)) return map.get(target)

  let clone
  // 特殊对象:Date / RegExp / Map / Set
  if (target instanceof Date) return new Date(target)
  if (target instanceof RegExp) return new RegExp(target.source, target.flags)
  if (target instanceof Map) {
    clone = new Map()
    map.set(target, clone)
    target.forEach((v, k) => clone.set(deepClone(k, map), deepClone(v, map)))
    return clone
  }
  if (target instanceof Set) {
    clone = new Set()
    map.set(target, clone)
    target.forEach(v => clone.add(deepClone(v, map)))
    return clone
  }

  // 数组和普通对象
  clone = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target))
  map.set(target, clone)

  for (const key of Reflect.ownKeys(target)) {      // 含 Symbol、不可枚举
    clone[key] = deepClone(target[key], map)
  }
  return clone
}

四道加分点:循环引用(WeakMap)、特殊对象(Date/RegExp/Map/Set)、Reflect.ownKeys(Symbol 键)、原型(Object.create)。

生产环境直接用 structuredClone(浏览器原生),面试再手写。

三、函数式工具 ​

3. 柯里化 curry ​

js
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args)
    return (...next) => curried.apply(this, [...args, ...next])
  }
}
// curry((a,b,c)=>a+b+c)(1)(2)(3) === 6
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args)
    return (...next) => curried.apply(this, [...args, ...next])
  }
}
// curry((a,b,c)=>a+b+c)(1)(2)(3) === 6

4. 组合 compose / pipe ​

js
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x)
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x)
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x)
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x)

四、数组与对象 ​

5. 数组扁平化 ​

js
// 递归
function flatten(arr) {
  return arr.reduce((acc, cur) =>
    acc.concat(Array.isArray(cur) ? flatten(cur) : cur), [])
}
// 原生(推荐)
arr.flat(Infinity)
// 递归
function flatten(arr) {
  return arr.reduce((acc, cur) =>
    acc.concat(Array.isArray(cur) ? flatten(cur) : cur), [])
}
// 原生(推荐)
arr.flat(Infinity)

6. 数组去重(含对象) ​

js
const uniq = (arr) => [...new Set(arr)]                    // 基本类型
const uniqBy = (arr, key) => {                             // 对象按字段
  const map = new Map()
  arr.forEach(item => { if (!map.has(item[key])) map.set(item[key], item) })
  return [...map.values()]
}
const uniq = (arr) => [...new Set(arr)]                    // 基本类型
const uniqBy = (arr, key) => {                             // 对象按字段
  const map = new Map()
  arr.forEach(item => { if (!map.has(item[key])) map.set(item[key], item) })
  return [...map.values()]
}

7. 数组转树 / 树转数组 ​

js
function listToTree(list, idKey = 'id', pidKey = 'pid', rootId = 0) {
  const map = new Map()
  list.forEach(item => map.set(item[idKey], { ...item, children: [] }))
  const tree = []
  map.forEach(item => {
    const parent = map.get(item[pidKey])
    if (parent) parent.children.push(item)
    else if (item[pidKey] === rootId) tree.push(item)
  })
  return tree
}

function treeToList(tree) {
  const out = []
  const walk = (nodes) => nodes.forEach(n => {
    out.push(n)
    if (n.children?.length) walk(n.children)
  })
  walk(tree)
  return out
}
function listToTree(list, idKey = 'id', pidKey = 'pid', rootId = 0) {
  const map = new Map()
  list.forEach(item => map.set(item[idKey], { ...item, children: [] }))
  const tree = []
  map.forEach(item => {
    const parent = map.get(item[pidKey])
    if (parent) parent.children.push(item)
    else if (item[pidKey] === rootId) tree.push(item)
  })
  return tree
}

function treeToList(tree) {
  const out = []
  const walk = (nodes) => nodes.forEach(n => {
    out.push(n)
    if (n.children?.length) walk(n.children)
  })
  walk(tree)
  return out
}

8. 安全取值 get(obj, 'a.b[0].c') ​

js
function get(obj, path, defaultValue) {
  const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.').filter(Boolean)
  let cur = obj
  for (const k of keys) {
    if (cur == null) return defaultValue
    cur = cur[k]
  }
  return cur === undefined ? defaultValue : cur
}
function get(obj, path, defaultValue) {
  const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.').filter(Boolean)
  let cur = obj
  for (const k of keys) {
    if (cur == null) return defaultValue
    cur = cur[k]
  }
  return cur === undefined ? defaultValue : cur
}

五、new / instanceof / call / apply / bind ​

9. new 的实现 ​

js
function myNew(Ctor, ...args) {
  if (typeof Ctor !== 'function') throw new TypeError('not a constructor')
  // 1. 创建对象,原型指向 Ctor.prototype
  const obj = Object.create(Ctor.prototype)
  // 2. 执行构造函数,this 指向新对象
  const res = Ctor.apply(obj, args)
  // 3. 构造函数返回对象则用它,否则用新对象
  return res !== null && typeof res === 'object' ? res : obj
}
function myNew(Ctor, ...args) {
  if (typeof Ctor !== 'function') throw new TypeError('not a constructor')
  // 1. 创建对象,原型指向 Ctor.prototype
  const obj = Object.create(Ctor.prototype)
  // 2. 执行构造函数,this 指向新对象
  const res = Ctor.apply(obj, args)
  // 3. 构造函数返回对象则用它,否则用新对象
  return res !== null && typeof res === 'object' ? res : obj
}

10. instanceof 的实现 ​

js
function myInstanceof(left, right) {
  if (typeof left !== 'object' || left === null) return false
  let proto = Object.getPrototypeOf(left)
  while (proto) {
    if (proto === right.prototype) return true
    proto = Object.getPrototypeOf(proto)    // 沿原型链上溯
  }
  return false
}
function myInstanceof(left, right) {
  if (typeof left !== 'object' || left === null) return false
  let proto = Object.getPrototypeOf(left)
  while (proto) {
    if (proto === right.prototype) return true
    proto = Object.getPrototypeOf(proto)    // 沿原型链上溯
  }
  return false
}

11. call / apply / bind ​

js
Function.prototype.myCall = function (ctx, ...args) {
  ctx = ctx == null ? globalThis : Object(ctx)
  const key = Symbol('fn')          // 用 Symbol 避免覆盖同名属性
  ctx[key] = this
  const res = ctx[key](...args)
  delete ctx[key]
  return res
}

Function.prototype.myApply = function (ctx, args = []) {
  return this.myCall(ctx, ...args)
}

Function.prototype.myBind = function (ctx, ...args) {
  const fn = this
  function Bound(...rest) {
    // 关键:用 new 调用时,this 指向实例而不是 ctx
    return fn.apply(this instanceof Bound ? this : ctx, [...args, ...rest])
  }
  Bound.prototype = Object.create(fn.prototype)   // 保留原型链
  return Bound
}
Function.prototype.myCall = function (ctx, ...args) {
  ctx = ctx == null ? globalThis : Object(ctx)
  const key = Symbol('fn')          // 用 Symbol 避免覆盖同名属性
  ctx[key] = this
  const res = ctx[key](...args)
  delete ctx[key]
  return res
}

Function.prototype.myApply = function (ctx, args = []) {
  return this.myCall(ctx, ...args)
}

Function.prototype.myBind = function (ctx, ...args) {
  const fn = this
  function Bound(...rest) {
    // 关键:用 new 调用时,this 指向实例而不是 ctx
    return fn.apply(this instanceof Bound ? this : ctx, [...args, ...rest])
  }
  Bound.prototype = Object.create(fn.prototype)   // 保留原型链
  return Bound
}

易错点:bind 要处理 new 调用的情况;call 用 Symbol 当临时键,别用 'fn'。

六、发布订阅 EventEmitter ​

js
class EventEmitter {
  constructor() { this.events = new Map() }

  on(name, fn) {
    if (!this.events.has(name)) this.events.set(name, new Set())
    this.events.get(name).add(fn)
    return () => this.off(name, fn)      // 返回取消函数
  }

  once(name, fn) {
    const wrap = (...args) => { fn(...args); this.off(name, wrap) }
    return this.on(name, wrap)
  }

  emit(name, ...args) {
    this.events.get(name)?.forEach(fn => fn(...args))
  }

  off(name, fn) {
    if (!fn) this.events.delete(name)
    else this.events.get(name)?.delete(fn)
  }
}
class EventEmitter {
  constructor() { this.events = new Map() }

  on(name, fn) {
    if (!this.events.has(name)) this.events.set(name, new Set())
    this.events.get(name).add(fn)
    return () => this.off(name, fn)      // 返回取消函数
  }

  once(name, fn) {
    const wrap = (...args) => { fn(...args); this.off(name, wrap) }
    return this.on(name, wrap)
  }

  emit(name, ...args) {
    this.events.get(name)?.forEach(fn => fn(...args))
  }

  off(name, fn) {
    if (!fn) this.events.delete(name)
    else this.events.get(name)?.delete(fn)
  }
}

用 Set 而不是数组:自动去重,删除不用 indexOf。

七、Promise ​

12. 手写 Promise(简化 A+ 版) ​

js
class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.callbacks = []

    const resolve = (v) => this.#settle('fulfilled', v)
    const reject = (e) => this.#settle('rejected', e)

    try { executor(resolve, reject) } catch (e) { reject(e) }
  }

  #settle(state, value) {
    if (this.state !== 'pending') return   // 状态只能改一次
    this.state = state
    this.value = value
    // 用微任务保证 then 回调异步执行
    queueMicrotask(() => this.callbacks.forEach(cb => cb(state, value)))
    this.callbacks = []
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const handle = (state, value) => {
        try {
          const cb = state === 'fulfilled' ? onFulfilled : onRejected
          if (typeof cb === 'function') resolve(cb(value))
          else state === 'fulfilled' ? resolve(value) : reject(value)
        } catch (e) { reject(e) }
      }
      if (this.state === 'pending') this.callbacks.push(handle)
      else queueMicrotask(() => handle(this.state, this.value))
    })
  }

  catch(onRejected) { return this.then(null, onRejected) }
}
class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.callbacks = []

    const resolve = (v) => this.#settle('fulfilled', v)
    const reject = (e) => this.#settle('rejected', e)

    try { executor(resolve, reject) } catch (e) { reject(e) }
  }

  #settle(state, value) {
    if (this.state !== 'pending') return   // 状态只能改一次
    this.state = state
    this.value = value
    // 用微任务保证 then 回调异步执行
    queueMicrotask(() => this.callbacks.forEach(cb => cb(state, value)))
    this.callbacks = []
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const handle = (state, value) => {
        try {
          const cb = state === 'fulfilled' ? onFulfilled : onRejected
          if (typeof cb === 'function') resolve(cb(value))
          else state === 'fulfilled' ? resolve(value) : reject(value)
        } catch (e) { reject(e) }
      }
      if (this.state === 'pending') this.callbacks.push(handle)
      else queueMicrotask(() => handle(this.state, this.value))
    })
  }

  catch(onRejected) { return this.then(null, onRejected) }
}

13. Promise.all / allSettled / race / any ​

js
function myAll(promises) {
  return new Promise((resolve, reject) => {
    const results = []
    let count = 0
    if (!promises.length) return resolve(results)
    promises.forEach((p, i) => {
      Promise.resolve(p).then(v => {
        results[i] = v                        // 用下标保证顺序
        if (++count === promises.length) resolve(results)
      }, reject)                              // 任一失败立即 reject
    })
  })
}

function myAllSettled(promises) {
  return Promise.all(promises.map(p =>
    Promise.resolve(p).then(
      value => ({ status: 'fulfilled', value }),
      reason => ({ status: 'rejected', reason }),
    )))
}
function myAll(promises) {
  return new Promise((resolve, reject) => {
    const results = []
    let count = 0
    if (!promises.length) return resolve(results)
    promises.forEach((p, i) => {
      Promise.resolve(p).then(v => {
        results[i] = v                        // 用下标保证顺序
        if (++count === promises.length) resolve(results)
      }, reject)                              // 任一失败立即 reject
    })
  })
}

function myAllSettled(promises) {
  return Promise.all(promises.map(p =>
    Promise.resolve(p).then(
      value => ({ status: 'fulfilled', value }),
      reason => ({ status: 'rejected', reason }),
    )))
}

14. 并发控制(限制并发数) ​

js
async function limitConcurrency(tasks, limit = 5) {
  const results = []
  let index = 0

  async function worker() {
    while (index < tasks.length) {
      const i = index++                    // 注意:先取下标再自增
      results[i] = await tasks[i]()
    }
  }

  await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker))
  return results
}
async function limitConcurrency(tasks, limit = 5) {
  const results = []
  let index = 0

  async function worker() {
    while (index < tasks.length) {
      const i = index++                    // 注意:先取下标再自增
      results[i] = await tasks[i]()
    }
  }

  await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker))
  return results
}

这是最高频的异步手写题,比手写 Promise 更常考。核心是「固定数量的 worker 抢任务」。

八、其他常考小工具 ​

js
// 重试
async function retry(fn, times = 3, delay = 300) {
  for (let i = 0; i < times; i++) {
    try { return await fn() } catch (e) {
      if (i === times - 1) throw e
      await new Promise(r => setTimeout(r, delay * 2 ** i))   // 指数退避
    }
  }
}

// sleep
const sleep = (ms) => new Promise(r => setTimeout(r, ms))

// 大数相加
function bigAdd(a, b) {
  let i = a.length - 1, j = b.length - 1, carry = 0, out = ''
  while (i >= 0 || j >= 0 || carry) {
    const sum = (+a[i--] || 0) + (+b[j--] || 0) + carry
    out = (sum % 10) + out
    carry = sum / 10 | 0
  }
  return out
}

// 模板替换
const render = (tpl, data) =>
  tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => data[k] ?? '')
// 重试
async function retry(fn, times = 3, delay = 300) {
  for (let i = 0; i < times; i++) {
    try { return await fn() } catch (e) {
      if (i === times - 1) throw e
      await new Promise(r => setTimeout(r, delay * 2 ** i))   // 指数退避
    }
  }
}

// sleep
const sleep = (ms) => new Promise(r => setTimeout(r, ms))

// 大数相加
function bigAdd(a, b) {
  let i = a.length - 1, j = b.length - 1, carry = 0, out = ''
  while (i >= 0 || j >= 0 || carry) {
    const sum = (+a[i--] || 0) + (+b[j--] || 0) + carry
    out = (sum % 10) + out
    carry = sum / 10 | 0
  }
  return out
}

// 模板替换
const render = (tpl, data) =>
  tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => data[k] ?? '')

九、手写题通用检查表 ​

写完任何一段,按这个过一遍:

  • [ ] this 有没有丢(fn.apply(this, args))
  • [ ] 参数有没有透传(...args)
  • [ ] 返回值对不对(防抖返回 result、new 返回对象判断)
  • [ ] 清理有没有做(定时器、事件监听、Symbol 临时键)
  • [ ] 边界:空数组、null、循环引用、并发为 0
  • [ ] 可取消:cancel / 返回解除函数(面试官最爱追问)

十、速查 ​

题一句话关键
防抖清掉上一个定时器,this 和 args 要透传
节流时间戳版丢最后一次,定时器版丢第一次
深拷贝WeakMap 处理循环引用 + 特殊对象 + Reflect.ownKeys
newObject.create(proto) + 返回对象判断
instanceof沿原型链上溯比较
bind用 new 调用时 this 指向实例
Promise状态不可逆 + then 异步 + 返回值穿透
并发控制固定 worker 数量 + 共享 index
EventEmitter用 Set 存回调,on 返回取消函数