手写代码题汇总
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)) // -19. 函数组合(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')) // HelloWorld11. 大数相加
题目:手写代码:实现两个大数相加(超过 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)