前端手写题集
面试让你「手写一个 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) === 6function 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) === 64. 组合 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 |
| new | Object.create(proto) + 返回对象判断 |
| instanceof | 沿原型链上溯比较 |
| bind | 用 new 调用时 this 指向实例 |
| Promise | 状态不可逆 + then 异步 + 返回值穿透 |
| 并发控制 | 固定 worker 数量 + 共享 index |
| EventEmitter | 用 Set 存回调,on 返回取消函数 |
Cao Note