Skip to content

手写代码题汇总

1. 防抖(Debounce)

题目:手写代码:实现一个防抖函数(debounce)

答案要点:

防抖是在事件触发后延迟执行,如果在延迟时间内再次触发,则重新计时。

  • 核心逻辑: 使用 setTimeout 延迟执行,并在每次触发时 clearTimeout
  • this 指向: 确保执行函数时的 this 指向正确
  • 参数传递: 支持透传事件对象等参数
  • 立即执行: 可选支持第一次触发是否立即执行

代码实现:

javascript
function debounce(fn, wait, immediate = false) {
  let timer = null

  return function (...args) {
    const context = this

    if (timer) clearTimeout(timer)

    if (immediate) {
      const callNow = !timer
      timer = setTimeout(() => {
        timer = null
      }, wait)
      if (callNow) fn.apply(context, args)
    } else {
      timer = setTimeout(() => {
        fn.apply(context, args)
      }, wait)
    }
  }
}

// 使用示例
const handleResize = debounce(() => {
  console.log('窗口大小改变')
}, 300)

window.addEventListener('resize', handleResize)

常见坑:

  • 忘记清除定时器
  • 箭头函数导致 this 绑定失效

2. 节流(Throttle)

题目:手写代码:实现一个节流函数(throttle)

答案要点:

节流是限制函数在一定时间内只能执行一次。

  • 时间戳方式: 记录上次执行时间,判断当前时间与上次执行的间隔
  • 定时器方式: 使用 setTimeout 控制执行频率
  • 结合方式: 首次立即执行,最后一次也执行

代码实现:

javascript
// 时间戳版本
function throttle(fn, wait) {
  let previous = 0

  return function (...args) {
    const now = Date.now()
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}

// 定时器版本
function throttle(fn, wait) {
  let timer = null

  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null
        fn.apply(this, args)
      }, wait)
    }
  }
}

// 结合版本(首次立即执行,最后一次也执行)
function throttle(fn, wait) {
  let previous = 0
  let timer = null

  return function (...args) {
    const now = Date.now()
    const remaining = wait - (now - previous)

    if (remaining <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      previous = now
      fn.apply(this, args)
    } else if (!timer) {
      timer = setTimeout(() => {
        previous = Date.now()
        timer = null
        fn.apply(this, args)
      }, remaining)
    }
  }
}

3. 深拷贝(Deep Clone)

题目:手写一个深拷贝函数,需要考虑循环引用和 Symbol 类型

答案要点:

深拷贝需要递归遍历对象的所有属性,并处理特殊类型以及避免循环引用导致的死循环。

  • 使用 WeakMap: 存储已拷贝的对象,解决循环引用问题
  • 使用 Reflect.ownKeys: 获取包括 Symbol 在内的所有键名
  • 区分处理: 数组、对象、Date、RegExp 等特殊引用类型
  • 递归终止: 条件是处理基本数据类型

代码实现:

javascript
function deepClone(obj, hash = new WeakMap()) {
  // 处理 null 或非对象类型
  if (obj === null || typeof obj !== 'object') return obj

  // 处理 Date
  if (obj instanceof Date) return new Date(obj)

  // 处理 RegExp
  if (obj instanceof RegExp) return new RegExp(obj)

  // 处理 Map
  if (obj instanceof Map) {
    const mapCopy = new Map()
    obj.forEach((value, key) => {
      mapCopy.set(deepClone(key, hash), deepClone(value, hash))
    })
    return mapCopy
  }

  // 处理 Set
  if (obj instanceof Set) {
    const setCopy = new Set()
    obj.forEach((value) => {
      setCopy.add(deepClone(value, hash))
    })
    return setCopy
  }

  // 处理循环引用
  if (hash.has(obj)) return hash.get(obj)

  // 创建新对象,保持原型链
  const cloneObj = new obj.constructor()
  hash.set(obj, cloneObj)

  // 使用 Reflect.ownKeys 获取所有键(包括 Symbol)
  Reflect.ownKeys(obj).forEach((key) => {
    cloneObj[key] = deepClone(obj[key], hash)
  })

  return cloneObj
}

// 测试
const obj = {
  a: 1,
  b: { c: 2 },
  d: [1, 2, 3],
  e: new Date(),
  f: /abc/g,
  g: new Map([['key', 'value']]),
  h: new Set([1, 2, 3]),
  [Symbol('sym')]: 'symbol value',
}
obj.circular = obj // 循环引用

const cloned = deepClone(obj)
console.log(cloned)

常见坑:

  • 使用 Map 而不是 WeakMap 导致内存无法被回收
  • 忽略了 Symbol 类型的属性拷贝

追问:

  • 为什么处理循环引用要用 WeakMap 而不是 Map?

4. 数组扁平化

题目:手写代码:实现数组扁平化(flat)

答案要点:

数组扁平化是将多维数组转换为一维数组的过程。

  • 递归方法: 遍历数组,遇到数组元素递归处理
  • reduce 方法: 使用 reduce 累加器实现
  • toString 方法: 利用 toString 特性(仅适用于纯数字数组)
  • ES2019 flat: 原生方法,可指定深度

代码实现:

javascript
// 递归方法
function flatten(arr) {
  const result = []
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result.push(...flatten(arr[i]))
    } else {
      result.push(arr[i])
    }
  }
  return result
}

// reduce 方法
function flatten(arr) {
  return arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatten(val) : val), [])
}

// 指定深度
function flatten(arr, depth = 1) {
  if (depth === 0) return arr
  return arr.reduce(
    (acc, val) => acc.concat(Array.isArray(val) ? flatten(val, depth - 1) : val),
    [],
  )
}

// 使用 Generator
function* flattenGenerator(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* flattenGenerator(item)
    } else {
      yield item
    }
  }
}

// 使用栈(迭代,避免栈溢出)
function flatten(arr) {
  const stack = [...arr]
  const result = []

  while (stack.length) {
    const next = stack.pop()
    if (Array.isArray(next)) {
      stack.push(...next)
    } else {
      result.push(next)
    }
  }

  return result.reverse()
}

5. Promise.all

题目:手写题:实现一个 Promise.all

答案要点:

Promise.all 接收一个 Promise 实例数组,只有当所有实例都成功时才返回成功结果数组,只要有一个失败就立即返回失败。

  • 输入处理: 需要处理非 Promise 类型的元素,将其包装为 Promise
  • 计数器机制: 维护一个计数器记录已完成的数量,当计数器等于数组长度时 resolve
  • 顺序保证: 结果数组的顺序必须与输入数组的顺序一致,不能按完成先后顺序排列
  • 错误处理: 任何一个 Promise 失败,直接 reject 整个结果

代码实现:

javascript
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Arguments must be an array'))
    }

    const results = []
    let completedCount = 0

    if (promises.length === 0) {
      resolve(results)
      return
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => {
          results[index] = value
          completedCount++
          if (completedCount === promises.length) {
            resolve(results)
          }
        },
        (reason) => reject(reason),
      )
    })
  })
}

// 使用示例
const p1 = Promise.resolve(1)
const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 100))
const p3 = 3 // 非 Promise 值

promiseAll([p1, p2, p3]).then((values) => {
  console.log(values) // [1, 2, 3]
})

常见坑:

  • 结果数组顺序错乱(直接用 push 而不是通过索引赋值)
  • 没有处理空数组的情况

6. 发布订阅模式(EventEmitter)

题目:手写代码:实现一个简单的 EventEmitter(发布订阅模式)

答案要点:

发布订阅模式是一种消息通信模式,订阅者订阅特定事件,发布者在事件发生时通知所有订阅者。

  • 事件存储: 使用对象存储事件名和对应的回调函数数组
  • 订阅(on): 将回调函数添加到对应事件的数组中
  • 发布(emit): 遍历对应事件的回调数组并执行
  • 取消订阅(off): 从回调数组中移除指定回调
  • 一次性订阅(once): 执行一次后自动取消订阅

代码实现:

javascript
class EventEmitter {
  constructor() {
    this.events = {}
  }

  // 订阅事件
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)

    // 返回取消订阅函数
    return () => this.off(event, callback)
  }

  // 发布事件
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach((callback) => {
        callback.apply(this, args)
      })
    }
  }

  // 取消订阅
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter((cb) => cb !== callback)
    }
  }

  // 一次性订阅
  once(event, callback) {
    const wrapper = (...args) => {
      callback.apply(this, args)
      this.off(event, wrapper)
    }
    this.on(event, wrapper)
  }

  // 移除所有订阅
  removeAllListeners(event) {
    if (event) {
      delete this.events[event]
    } else {
      this.events = {}
    }
  }
}

// 使用示例
const emitter = new EventEmitter()

const unsubscribe = emitter.on('message', (data) => {
  console.log('收到消息:', data)
})

emitter.emit('message', 'Hello World') // 收到消息: Hello World

unsubscribe() // 取消订阅

emitter.once('init', () => {
  console.log('初始化完成')
})

emitter.emit('init') // 初始化完成
emitter.emit('init') // 无输出

7. 柯里化(Currying)

题目:手写代码:实现一个函数柯里化

答案要点:

柯里化是将一个多参数函数转换为一系列单参数函数的技术。

代码实现:

javascript
// 基础柯里化
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2))
      }
    }
  }
}

// 使用示例
function add(a, b, c) {
  return a + b + c
}

const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6

// 支持占位符的柯里化
function curryWithPlaceholder(fn, placeholder = '_') {
  return function curried(...args) {
    if (args.length >= fn.length && !args.includes(placeholder)) {
      return fn.apply(this, args)
    } else {
      return function (...args2) {
        const mergedArgs = args.map((arg) =>
          arg === placeholder && args2.length ? args2.shift() : arg,
        )
        return curried.apply(this, [...mergedArgs, ...args2])
      }
    }
  }
}

8. LRU 缓存

题目:手写代码:实现一个 LRU(最近最少使用)缓存

答案要点:

LRU 缓存是一种在容量满时淘汰最久未使用数据的缓存策略。

  • 使用 Map: 利用 Map 的插入顺序特性,最新使用的放在最后
  • get 操作: 获取值后需要重新插入以保持最新
  • put 操作: 插入新值,如果超过容量则删除第一个(最久未使用)

代码实现:

javascript
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.cache = new Map()
  }

  get(key) {
    if (this.cache.has(key)) {
      // 移动到最新
      const value = this.cache.get(key)
      this.cache.delete(key)
      this.cache.set(key, value)
      return value
    }
    return -1
  }

  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    } else if (this.cache.size >= this.capacity) {
      // 删除最久未使用的(Map 的第一个元素)
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    this.cache.set(key, value)
  }
}

// 使用示例
const cache = new LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
console.log(cache.get(1)) // 1
cache.put(3, 3) // 淘汰 key 2
console.log(cache.get(2)) // -1

9. 函数组合(Compose / Pipe)

题目:手写代码:实现函数组合 compose 和 pipe

答案要点:

函数组合是将多个函数组合成一个函数,前一个函数的输出作为后一个函数的输入。

  • compose: 从右到左执行函数
  • pipe: 从左到右执行函数

代码实现:

javascript
// compose: 从右到左
const compose = (...fns) => {
  return (initialValue) => {
    return fns.reduceRight((acc, fn) => fn(acc), initialValue)
  }
}

// pipe: 从左到右
const pipe = (...fns) => {
  return (initialValue) => {
    return fns.reduce((acc, fn) => fn(acc), initialValue)
  }
}

// 使用示例
const add5 = (x) => x + 5
const multiply2 = (x) => x * 2
const toString = (x) => String(x)

const composed = compose(toString, multiply2, add5)
console.log(composed(3)) // "16" (3+5=8, 8*2=16, "16")

const piped = pipe(add5, multiply2, toString)
console.log(piped(3)) // "16" (3+5=8, 8*2=16, "16")

10. 字符串转驼峰

题目:手写代码:将 kebab-case 字符串转换为 camelCase

代码实现:

javascript
function kebabToCamel(str) {
  return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase())
}

// 大驼峰(PascalCase)
function kebabToPascal(str) {
  const camel = kebabToCamel(str)
  return camel.charAt(0).toUpperCase() + camel.slice(1)
}

console.log(kebabToCamel('hello-world')) // helloWorld
console.log(kebabToPascal('hello-world')) // HelloWorld

11. 大数相加

题目:手写代码:实现两个大数相加(超过 Number 安全整数范围)

代码实现:

javascript
function addBigNumbers(a, b) {
  let result = ''
  let carry = 0

  // 补齐长度
  const maxLength = Math.max(a.length, b.length)
  a = a.padStart(maxLength, '0')
  b = b.padStart(maxLength, '0')

  // 从右向左逐位相加
  for (let i = maxLength - 1; i >= 0; i--) {
    const sum = parseInt(a[i]) + parseInt(b[i]) + carry
    carry = Math.floor(sum / 10)
    result = (sum % 10) + result
  }

  // 处理最后的进位
  if (carry) {
    result = carry + result
  }

  return result
}

console.log(addBigNumbers('999999999999999999', '1'))
// "1000000000000000000"

12. 模拟 setInterval

题目:手写代码:使用 setTimeout 模拟 setInterval

答案要点:

使用 setTimeout 递归调用可以模拟 setInterval,而且可以更精确地控制执行间隔。

代码实现:

javascript
function mySetInterval(fn, delay) {
  let timer = null

  function loop() {
    timer = setTimeout(() => {
      fn()
      loop()
    }, delay)
  }

  loop()

  // 返回取消函数
  return {
    clear: () => clearTimeout(timer),
  }
}

// 使用
const interval = mySetInterval(() => {
  console.log('执行')
}, 1000)

// 5秒后停止
setTimeout(() => {
  interval.clear()
}, 5000)