# 面试高频手写题
手写题别急着背完整代码。先用一句话说清输入、输出和最容易踩的边界,再关掉答案自己写一遍;写完拿空值、重复调用和异步顺序试一试。面试时就算没写完,能把思路和取舍讲明白,也比默写一段记不住的实现更有帮助。
建议优先掌握:
instanceof- 考察对原型链的理解new- 对创建对象实例过程的理解call/apply/bind- 对this指向的理解- 手写
promise- 对异步的理解 - 手写原生
ajax- 对ajax原理和http请求方式的理解,重点是get和post请求的实现
# 1 实现防抖函数(debounce)
30 秒速记
- 防抖 = 电梯关门:有人进来就重新等,停止触发
wait毫秒后才执行最后一次 - 核心三步:
clearTimeout(timer)→timer = setTimeout(...)→ 到点fn.apply(this, args) - 返回普通函数(不是箭头函数)才能拿到调用时的
this;参数用...args传最后一次的 - 进阶:
immediate首次立即执行、cancel()取消、flush()立刻执行挂起的那次 - 场景:搜索联想、输入校验、
resize结束后重算;React里要用useMemo/useRef保持同一个防抖实例
防抖就是事件连续触发时一直重新计时,停下来超过 wait 才执行一次。 像电梯关门,只要还有人进来就重新等。实现就是闭包里存一个 timer,每次调用先 clearTimeout,再开新的 setTimeout,到点用 fn.apply(this, args) 执行,这样 this 和最后一次的参数都对。和节流的区别是:防抖只要最后一次,持续触发就一直不执行;节流是按固定间隔执行。React 里有个常见坑,每次渲染都 debounce(fn) 出一个新函数,计时器各自独立,等于没防。
防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行
eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
手写简化版:
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
适用场景:
- 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
面试时写出基础版之后,面试官多半会追问「第一次能不能立刻执行」「能不能取消」。下面这版把两点都补上,代码不长,建议能默写:
function debounce(fn, wait = 300, immediate = false) {
let timer = null
function debounced(...args) {
// 立即执行版:当前没有计时器,说明是一串触发里的第一次
const callNow = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
// 非立即版在这里执行最后一次
if (!immediate) fn.apply(this, args)
}, wait)
if (callNow) return fn.apply(this, args)
}
debounced.cancel = () => {
clearTimeout(timer)
timer = null
}
return debounced
}
const log = debounce((v) => console.log('search', v), 300)
log('a'); log('ab'); log('abc')
// 300ms 后只输出一次:search abc
const submit = debounce(() => console.log('submit'), 1000, true)
submit(); submit(); submit()
// 立刻输出一次 submit,1 秒内后面两次被吞掉
几个容易丢分的点:返回的 debounced 必须是普通函数,箭头函数拿不到调用时的 this;定时器回调用箭头函数,才能沿用外层的 this 和 args;立即执行版要在定时器到点时把 timer 置空,否则下一轮第一次触发不会立即执行。在 React 里使用时,防抖实例要用 useMemo 或 useRef 固定住,组件卸载时调用 cancel(),避免卸载后还去 setState。
面试官追问
追问 1搜索框为什么用防抖不用节流?
产品要的是用户停下来后只查最后一次。节流在输入过程中每隔一段时间就会发一次,中间那些请求都是浪费。防抖的代价是停顿前看不到中间结果,一般 300ms 左右体感就可以。
追问 2组件里写 const onSearch = debounce(fetchList, 300),结果每个字都发了请求,为什么?
每次渲染都创建一个新的防抖函数,新函数清不掉旧函数的 timer,等于没防。用 useMemo(() => debounce(fetchList, 300), []) 或 useRef 保持同一个实例,卸载时调 cancel()。
追问 3提交按钮想防重复点击,但第一次点击要立刻有反应,怎么改?
尾部防抖会让第一次点击延迟 wait 才执行。要用立即执行版:timer 为空时直接执行,然后开计时器,wait 内的点击只重置计时。不过防重复提交更靠谱的是请求期间禁用按钮,再加服务端幂等。
追问 4防抖后的函数有返回值,调用方拿得到吗?
尾部执行版拿不到,原函数是在定时器里执行的,调用时它还没跑。立即执行版可以同步返回;要拿尾部那次的结果,就让防抖函数返回一个 Promise,到点后 resolve。
追问 5搜索联想加了防抖,偶尔还是显示上一个关键词的结果,为什么?
防抖只减少请求数,管不了返回顺序。前一个请求慢、后一个快,旧结果后到就把新结果盖了。每次请求带上序号,返回时不是最新的就丢弃,或者用 AbortController 取消上一个请求。
# 2 实现节流函数(throttle)
30 秒速记
- 节流 = 水龙头限流:持续触发时每隔
wait最多执行一次,不会一直拖着不执行 - 时间戳版:
now - last >= wait才执行,首次立即执行,但停止前的最后一次可能丢 - 定时器版:没有挂起任务时才开
setTimeout,首次要等wait,停止后还会补一次 - 完整版两者结合:首次立即、末尾补一次(
lodash的leading/trailing) - 场景:滚动加载、拖拽、
resize、埋点上报;和视觉动画相关的优先考虑requestAnimationFrame
节流是不管事件触发多频繁,每隔一段时间最多只执行一次。 有两种写法:时间戳版记录上次执行时间,间隔够了就执行,所以第一次立刻响应,但停止触发前最后那次如果没到间隔就丢了;定时器版是没有挂起任务时才开一个 setTimeout,第一次要等,停下后还会补执行一次。拖拽这种既要跟手又要落到终点的场景,单用哪种都不完整,得把两种结合起来。和防抖比,节流能在持续触发的过程中有节奏地给反馈,滚动加载、拖拽就该用它。
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是:事件,按照一段时间的间隔来进行触发。

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的时间
let lastTime = 0
return function(...args) {
// 当前时间
let now = +new Date()
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this