作为一名现代前端开发人员,除了掌握框架(React/Vue/Next.js等)和构建工具(Vite/Webpack),你还需要熟练掌握或手写一大批实用工具函数(Utils),这些函数几乎出现在每一个中大型项目中。
下面是一张目前最常用、最值得掌握的前端工具函数分类表格(2025~2026主流实践):
| 分类 | 函数名称/功能 | 作用/典型使用场景 | 推荐掌握程度 | 是否建议自己手写一次 | 常用替代库 |
|---|---|---|---|---|---|
| 类型判断 | isObject / isPlainObject | 判断纯对象(排除null、数组、Date等) | ★★★★★ | 是 | Lodash.isPlainObject |
| 类型判断 | isEmpty / isNil | 判断空值(null/undefined/空对象/空数组/空字符串) | ★★★★★ | 是 | Lodash/Ramda |
| 类型判断 | isBrowser / isServer | 判断当前运行环境(浏览器/服务端) | ★★★★ | 是 | — |
| 防抖/节流 | debounce / throttle | 搜索框、resize、scroll、按钮狂点等 | ★★★★★ | 强烈建议手写 | lodash.debounce |
| 深拷贝 | deepClone / structuredClone | 对象/数组深拷贝(支持循环引用更好) | ★★★★★ | 是(至少掌握一种) | lodash.cloneDeep |
| 对象操作 | omit / pick | 从对象中删除/保留指定字段 | ★★★★ | 是 | Lodash |
| 对象操作 | merge / deepMerge | 对象深度合并(常用于配置合并) | ★★★★ | 是 | Lodash.merge |
| 数组操作 | unique / uniqBy | 数组去重(基础/根据字段) | ★★★★ | 是 | Lodash |
| 数组操作 | groupBy / countBy | 数组分组/计数 | ★★★ | 建议 | Lodash |
| 字符串处理 | capitalize / camelCase / kebabCase | 首字母大写 / 驼峰 / 连字符转换 | ★★★★ | 是 | Lodash |
| 字符串处理 | truncate / ellipsis | 字符串截断加省略号 | ★★★ | 是 | — |
| 函数式工具 | once / memoize | 函数只执行一次 / 记忆函数 | ★★★★ | 是 | Lodash |
| URL/参数 | getQueryParam / parseUrl / stringifyQuery | 获取/解析/序列化URL参数 | ★★★★★ | 是 | qs |
| Cookie | getCookie / setCookie / removeCookie | cookie读写删除(兼容性处理) | ★★★★ | 是 | js-cookie |
| Storage | storage.get / storage.set(带过期) | localStorage/sessionStorage 带过期时间 | ★★★★ | 是 | — |
| 时间日期 | formatDate / relativeTime / dayDiff | 日期格式化 / 多久前 / 相差几天 | ★★★★ | 是 | date-fns / dayjs |
| 随机 | randomId / uuid / randomString | 生成唯一id / 随机字符串 | ★★★★ | 是 | uuid / nanoid |
| 颜色处理 | hexToRgb / rgbToHex / lightenColor | 颜色格式转换 / 亮度调整 | ★★★ | 视项目而定 | tinycolor2 |
| 设备/浏览器 | isMobile / isWechat / getOS | 判断移动端/微信/操作系统 | ★★★★ | 是 | bowser / ua-parser |
| 性能优化 | requestIdleCallback polyfill | 空闲时执行任务 | ★★★ | 了解即可 | — |
| 事件相关 | on / off / once(事件总线) | 简易自定义事件 | ★★★ | 建议 | mitt / tiny-emitter |
| 错误处理 | tryCatch / safeJsonParse | 安全JSON.parse / 带try-catch的函数执行 | ★★★ | 是 | — |
| Promise | sleep / retry / timeout / parallelLimit | 延迟 / 重试 / 超时 / 限制并发 | ★★★★ | 是(至少sleep+retry) | p-limit / retry |
utils.js / helpers.ts 文件中这些函数看似简单,但真正能又快又稳地写出来,并且考虑边界情况、类型安全、性能的同学,在面试和实际项目中都非常吃香。
你现在最想先掌握/手写哪个分类的函数?可以告诉我,我给你出个手写题或参考实现~ 🚀
我对防抖和节流,真的是没有搞清楚,用法没有搞清楚、实现更没有搞清楚。原因在于我不知道二者到底有什么区别?是怎么实现的?实现之后的效果到底是什么?
我好像笼统的认为,都是隔一段时间之后再执行,二者好像没有区别啊?2020年作用收集的司徒正美的一篇文章,感觉也没有讲清楚。干脆直接问AI吧,结果AI一下子就抓住了重点。
防抖(Debounce)和节流(Throttle)的目的都是限制函数在短时间内被过于频繁地调用,但它们的策略和使用场景完全不同。
用最生活化的比喻来帮你一次搞懂两者的区别:
| 比喻场景 | 防抖(Debounce) | 节流(Throttle) | 核心区别 |
|---|---|---|---|
| 你在电梯门口狂按按钮 | 只会在你最后一次按完过了一段时间才发车 | 每隔几秒强制发一次车,不管你按多少次 | 防抖等你停手,节流强制间隔 |
| 搜索框输入关键词 | 你停下来不打字了才去发请求(最后一次输入后) | 你边打边搜,但每秒最多搜一次 | 防抖等安静,节流匀速放行 |
| 拖拽调整窗口大小 | 拖完松手才重新计算布局 | 拖拽过程中每隔200ms计算一次布局 | 防抖只认最后,节流要中间结果 |
| 鼠标疯狂快速移动 | 只在你停下来后才记录最后的位置 | 每隔一段时间就记录一次当前位置 | 防抖只取终点,节流要过程点 |
| 像不像疯狂发微信消息 | 一直发一直不显示,等你停几秒才一次性发出 | 一直发,但每秒最多只显示一条 | — |
| 最终触发次数 | 通常只触发1次(最后一次) | 可触发多次,但频率被严格控制 | 次数差别很大 |
| 典型等待时间感 | “你再等等我,我等你彻底停下来再说” | “你随便按,我每隔几秒给你处理一次” | 体验完全不同 |
| 场景 | 防抖(Debounce)合适吗? | 节流(Throttle)更合适吗? | 推荐选择 |
|---|---|---|---|
| 搜索框输入后发请求 | ★★★★★(强烈推荐) | ★★ | 防抖 |
| 窗口resize事件 | ★★★★ | ★★★★ | 看需求(大多数用节流) |
| 鼠标滚轮/触屏滑动 | ★★ | ★★★★★(强烈推荐) | 节流 |
| 按钮疯狂点击(防止重复提交) | ★★★★★ | ★★ | 防抖 |
| 拖拽元素实时跟随鼠标 | ★ | ★★★★★ | 节流 |
| input change 频繁校验 | ★★★★ | ★★★ | 多数用防抖 |
| 页面滚动加载更多 | ★★ | ★★★★★ | 节流 |
x1// 防抖:你停下来才算数2function debounce(fn, wait = 300) {3 let timer = null4 return function (args) {5 clearTimeout(timer) // 每次触发都把上次的清掉6 timer = setTimeout(() => {7 fn.apply(this, args) // 等你停了才执行8 }, wait)9 }10}1112// 节流:固定节奏放行13function throttle(fn, wait = 300) {14 let lastTime = 015 return function (args) {16 const now = Date.now()17 if (now - lastTime >= wait) { // 距离上次执行已经够间隔了18 fn.apply(this, args)19 lastTime = now20 }21 }22}一句话总结目前最容易记住的区别:
防抖 = “等你消停了再说” → 适合“最后一次才重要”的场景 节流 = “别太快,我要喘口气” → 适合“过程需要持续采样”的场景
我觉得这个是我最没有搞清楚的地方,只要搞清楚了这个,写代码实现就很容易了。
防抖(debounce)和节流(throttle)最终执行的是哪一次触发的函数?
这是一个非常容易混淆但又很核心的区别,下面用最直白的方式对比说明:
| 情况 | 防抖(debounce)最终执行的是哪一次 | 节流(throttle)最终执行的是哪一次 | 形象比喻 |
|---|---|---|---|
| 事件连续快速触发很多次 | 最后一次(你停手后的那一次) | 每隔固定时间段内的第一次(或最后一次,看实现) | 防抖:等你按完电梯才发车 节流:每分钟发一班车 |
| 假设间隔 300ms | 只在最后一次触发后等 300ms 才执行 | 大约每 300ms 执行一次(取决于 leading/trailing) | — |
| 典型例子:输入框搜索 | 你停下来不打字后的那次输入 | 你打字过程中每隔 300ms 取一次当前输入 | 防抖等你安静 节流边打边采样 |
| 典型例子:窗口 resize | 拖完窗口松手后的那次尺寸 | 拖动过程中每隔 300ms 取一次当前尺寸 | 防抖只认最终 节流要中间结果 |
| 假设连续触发 10 次 | 只执行第 10 次 | 可能执行 3~4 次(看间隔和持续时间) | — |
| 事件停止后是否还会执行 | 会(延迟后执行最后一次) | 取决于 trailing 设置(通常会补最后一次) | — |
xxxxxxxxxx471// 1. 防抖(最常见写法:trailing 模式)2function debounce(fn, delay) {3 let timer = null;4 return function (args) {5 clearTimeout(timer);6 timer = setTimeout(() => {7 fn.apply(this, args); // ← 永远是「最后一次」触发的参数8 }, delay);9 };10}1112// 2. 节流 - trailing 模式(最常见)13function throttle(fn, delay) {14 let lastTime = 0;15 return function (args) {16 const now = Date.now();17 if (now - lastTime >= delay) {18 fn.apply(this, args); // ← 这次时间窗里的「第一次」19 lastTime = now;20 }21 };22}2324// 3. 节流 - leading + trailing 模式(更完整)25function throttle(fn, delay, { leading = true, trailing = true } = {}) {26 let timer = null;27 let lastArgs = null;28 29 return function (args) {30 const now = Date.now();31 lastArgs = args;32 33 if (!timer && leading) {34 fn.apply(this, args); // ← 第一次立即执行35 }36 37 if (timer) clearTimeout(timer);38 39 timer = setTimeout(() => {40 if (trailing && lastArgs) {41 fn.apply(this, lastArgs); // ← 时间窗结束时的「最后一次」42 }43 timer = null;44 lastArgs = null;45 }, delay);46 };47}| 实现方式 | 最终执行的是哪一次调用 | 常见使用场景 |
|---|---|---|
| debounce(trailing) | 最后一次 | 搜索框、表单校验、保存按钮 |
| throttle(只 leading) | 每段间隔的第一次 | 滚动加载、鼠标移动跟踪 |
| throttle(只 trailing) | 每段间隔的最后一次 | 某些 resize 场景 |
| throttle(leading + trailing) | 第一 + 最后(中间丢弃) | 大多数现代 UI 组件库的默认节流 |
一句话总结区别(背下来最有用):
希望这次解释能让你彻底分清「到底执行的是哪一次」~ 如果还有具体场景不确定执行的是哪一次,可以举例告诉我,我帮你分析!
tanstack/react-pacer提供了很多实用的工具函数,我使用里面提供的防抖、节流函数来演示一下效果。这篇blog写的也很好,动画可以看一下https://kettanaito.com/blog/debounce-vs-throttle。
xxxxxxxxxx781import { useDebouncedCallback } from "@tanstack/react-pacer";2import { useRef, useState } from "react";34type User = {5 id: number;6 name: string;7};89export default function DebouncePage() {10 const [users, setUsers] = useState<User[]>([]);11 const [loading, setLoading] = useState(false);12 const controller = useRef(new AbortController());1314 const debounceSearch = useDebouncedCallback(15 async function handleSearch(query: string) {16 controller.current.abort();17 controller.current = new AbortController();18 setLoading(true);1920 const users = await fetchUsers(query, controller.current.signal);21 setUsers(users);22 setLoading(false);23 },24 {25 wait: 1000,26 }27 );2829 // async function handleSearch(query: string) {30 // controller.current.abort();31 // controller.current = new AbortController();32 // setLoading(true);3334 // const users = await fetchUsers(query, controller.current.signal);35 // setUsers(users);36 // setLoading(false);37 // }3839 return (40 <div className="bg-gray-100 w-100 px-4 py-8 rounded-2xl">41 <h1 className="mb-4 text-2xl">User Search</h1>4243 <div>44 <input45 className="border border-gray-400 rounded-xl w-full p-2"46 type="text"47 onChange={(e) => debounceSearch(e.target.value)}48 placeholder="Search users..."49 />50 </div>5152 {loading ? (53 <div className="text-left pt-3">Loading...</div>54 ) : (55 <ul>56 {users.map((user) => (57 <li58 key={user.id}59 className="border-b border-b-gray-200 p-2 text-gray-900 text-left cursor-pointer hover:bg-gray-200 transition-all duration-200">60 <div>{user.name}</div>61 </li>62 ))}63 </ul>64 )}65 </div>66 );67}6869async function fetchUsers(query: string, signal: AbortSignal) {70 const url = new URL("http://192.168.31.198:4000/users");71 if (query) url.searchParams.append("name_like", query);7273 // await new Promise((resolve) => setTimeout(resolve, 1000));7475 return fetch(url.toString(), { signal })76 .then((res) => res.json())77 .then((data) => data.data as User[]);78}这是没有加防抖的情况,输入框每次变动,都会请求一次数据。

这是加了防抖的,只有在输入停止1s之后才会请求数据。

xxxxxxxxxx201import { useEffect, useState } from "react";2import { useThrottledValue } from "@tanstack/react-pacer";34export default function ThrottlePage() {5 const [width, setWidth] = useState(window.innerWidth);6 const [throttledWidth] = useThrottledValue(width, { wait: 1000 });78 useEffect(() => {9 const controller = new AbortController();10 window.addEventListener("resize", () => setWidth(window.innerWidth), {11 signal: controller.signal,12 });13 }, []);14 return (15 <div className="bg-gray-100 rounded-2xl p-6 w-full">16 <h2 className="text-2xl">Width: {width}px</h2>17 <h2 className="text-2xl">ThrottledWidth: {throttledWidth}px</h2>18 </div>19 );20}可以看到,节流的实际效果就是每隔一段时间执行一次,不需要停止下来之后才能执行。

下面是简洁的表格概览,后面会逐一详细讲解作用、使用场景、核心原理、手写代码,帮助你真正理解和记住。
| 序号 | 名称 | 核心作用一句话总结 | 考察频率 | 手写难度 |
|---|---|---|---|---|
| 1 | debounce / throttle | 控制函数执行频率,防止高频事件卡顿/浪费资源 | ★★★★★ | 中等 |
| 2 | useDebounce | React 中安全防抖一个值 | ★★★★★ | 中等 |
| 3 | usePrevious | 获取上一次渲染的值 | ★★★★☆ | 低-中等 |
| 4 | deepClone | 完整深拷贝对象(处理循环引用) | ★★★★ | 中等 |
| 5 | useMemoizedFn | 创建永不变化的函数引用 | ★★★★ | 中等 |
| 6 | useUpdate | 强制触发组件重渲染 | ★★★★ | 低 |
| 7 | get (safe get) | 安全访问深层嵌套属性 | ★★★★ | 低-中等 |
| 8 | once | 让函数只被执行一次 | ★★★ | 低 |
详细作用
典型真实场景(非常详细)
debounce
throttle
核心区别(背下来) debounce = “等你消停了再说”(只执行最后一次) throttle = “别太快,我要喘口气”(固定节奏执行,过程有采样)
xxxxxxxxxx361// 防抖(最常用 trailing 版)2function debounce<T extends (args: any[]) => any>(3 fn: T,4 delay: number5): (args: Parameters<T>) => void {6 let timer: NodeJS.Timeout | null = null;7 return (args) => {8 if (timer) clearTimeout(timer);9 timer = setTimeout(() => fn(args), delay);10 };11}1213// 节流(leading + trailing 完整版)14function throttle<T extends (args: any[]) => any>(15 fn: T,16 delay: number,17 { leading = true, trailing = true } = {}18) {19 let timer: NodeJS.Timeout | null = null;20 let lastArgs: Parameters<T> | null = null;2122 return (args: Parameters<T>) => {23 lastArgs = args;2425 if (!timer && leading) {26 fn(args);27 }2829 if (timer) clearTimeout(timer);30 timer = setTimeout(() => {31 if (trailing && lastArgs) fn(lastArgs);32 timer = null;33 lastArgs = null;34 }, delay);35 };36}详细作用 在 React 中对值进行防抖处理,返回一个延迟更新的版本。 核心目的:避免值频繁变化导致 useEffect、API 调用、渲染等副作用频繁执行。
典型场景
xxxxxxxxxx191function useDebounce<T>(value: T, delay: number): T {2 const [debouncedValue, setDebouncedValue] = useState(value);34 useEffect(() => {5 const timer = setTimeout(() => {6 setDebouncedValue(value);7 }, delay);89 return () => clearTimeout(timer);10 }, [value, delay]);1112 return debouncedValue;13}1415// 用法16const debouncedSearch = useDebounce(searchText, 300);17useEffect(() => {18 if (debouncedSearch) fetchData(debouncedSearch);19}, [debouncedSearch]);详细作用 记住组件上一次渲染时的值,让你在当前渲染周期内可以拿到“旧值”进行对比。
典型场景
xxxxxxxxxx171function usePrevious<T>(value: T): T | undefined {2 const ref = useRef<T>();34 useEffect(() => {5 ref.current = value;6 }, [value]);78 return ref.current;9}1011// 用法示例12const prevCount = usePrevious(count);13useEffect(() => {14 if (prevCount !== undefined && count > prevCount) {15 console.log("count 增加了");16 }17}, [count]);详细作用 创建对象的完整独立副本,修改副本不会影响原对象,支持数组、嵌套对象、循环引用。
典型场景
xxxxxxxxxx171function deepClone<T>(obj: T, visited = new WeakMap()): T {2 if (obj === null || typeof obj !== "object") return obj;34 // 处理循环引用5 if (visited.has(obj)) return visited.get(obj);67 const clone = Array.isArray(obj) ? [] : {};8 visited.set(obj, clone);910 for (const key in obj) {11 if (Object.prototype.hasOwnProperty.call(obj, key)) {12 (clone as any)[key] = deepClone((obj as any)[key], visited);13 }14 }1516 return clone as T;17}详细作用 创建一个引用永远不变的函数,但内部永远执行最新传入的函数体。 解决了 useCallback 必须依赖 deps 数组,导致函数引用频繁变化的问题。
典型场景
xxxxxxxxxx121function useMemoizedFn<T extends (args: any[]) => any>(fn: T): T {2 const fnRef = useRef<T>(fn);34 // 每次渲染都更新最新的 fn,但 ref 本身不变5 fnRef.current = fn;67 const memoizedFn = useRef((args: Parameters<T>) => {8 return fnRef.current(args);9 });1011 return memoizedFn.current as T;12}详细作用 不修改任何状态的情况下,强制触发组件重新渲染。
典型场景
xxxxxxxxxx91function useUpdate() {2 const [, setState] = useState({});3 return useCallback(() => setState({}), []);4}56// 另一种更流行的写法(更省内存)7function useUpdate() {8 return useReducer(x => x + 1, 0)[1];9}详细作用 安全地访问对象深层属性,任意一层为 null/undefined 都不会报错,并可提供默认值。
典型场景
xxxxxxxxxx191function get<T>(2 obj: any,3 path: string | string[],4 defaultValue?: T5): T | undefined {6 const paths = Array.isArray(path) ? path : path.split(/[\.\[\]]/).filter(Boolean);7 8 let current = obj;9 for (const key of paths) {10 if (current == null) return defaultValue;11 current = current[key];12 }13 14 return current ?? defaultValue;15}1617// 用法18get(user, "profile.address.city", "未知城市");19get(obj, ["data", "user", 0, "name"]);详细作用 保证一个函数在整个生命周期(或指定范围内)只被执行一次,之后调用都返回第一次的结果。
典型场景
xxxxxxxxxx161function once<T extends (args: any[]) => any>(fn: T): (args: Parameters<T>) => ReturnType<T> | undefined {2 let called = false;3 let result: ReturnType<T>;45 return (args) => {6 if (called) return result;7 called = true;8 result = fn(args);9 return result;10 };11}1213// React 中常用写法(结合 useRef)14const init = useRef(once(() => {15 console.log("只初始化一次");16}));如果你准备面试,这些函数建议按照以下优先级手写练习:
需要我针对其中某一个再给出更详细的面试回答模板或常见追问吗?