
首先建好utils目录结构写任何业务代码之前都要先把工具函数库的骨架搭好src/ └── utils/ ├── debounce.js ├── formatDate.js ├── safeJsonParse.js ├── isEmpty.js ├── copyToClipboard.js ├── async.js ├── array.js ├── download.js ├── deep-clone.js ├── url.js └── index.js // 统一导出1、防抖DebounceDebounce应该是用得最多的一个没有之一。搜索框、滚动监听、窗口 resize任何用户操作会高频触发的场景都需要它。/** * 防抖函数 * 在停止调用 delay 毫秒后执行 func期间再次调用会重新计时 * param {Function} func - 需要防抖的函数 * param {number} delay - 等待时间毫秒默认 300 */ export function debounce(func, delay 300) { let timeout null; return function (...args) { if (timeout ! null) { clearTimeout(timeout); } timeout setTimeout(() { func(...args); timeout null; }, delay); }; } export default debounce;调用方式// 搜索框输入防抖停止输入 300ms 后才触发搜索 const onSearch debounce((keyword) { fetch(/api/search?q${keyword}); }, 300); input.addEventListener(input, (e) onSearch(e.target.value)); // 前沿触发点击立即响应之后冷却期内忽略 const onClick debounce(sendReq, 500, { leading: true, trailing: false }); // 取消 onSearch.cancel();2、日期格式化formatDate/** * 格式化日期 * param {Date|number|string} date - 日期对象、时间戳或日期字符串 * param {string} format - 格式模板默认 yyyy-MM-dd HH:mm:ss * 支持yyyy yy MM dd HH hh mm ss * returns {string} 格式化后的日期字符串 */ export function formatDate(date, format yyyy-MM-dd HH:mm:ss) { const d date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) { throw new RangeError(formatDate: 无效的日期值); } const map { yyyy: d.getFullYear(), yy: String(d.getFullYear()).slice(-2), MM: String(d.getMonth() 1).padStart(2, 0), dd: String(d.getDate()).padStart(2, 0), HH: String(d.getHours()).padStart(2, 0), hh: String(d.getHours() % 12 || 12).padStart(2, 0), mm: String(d.getMinutes()).padStart(2, 0), ss: String(d.getSeconds()).padStart(2, 0), }; return format.replace(/yyyy|yy|MM|dd|HH|hh|mm|ss/g, (key) map[key]); } /** * 常用格式快捷方法 */ export function dateFormat(date) { return formatDate(date, yyyy-MM-dd); } export function datetimeFormat(date) { return formatDate(date, yyyy-MM-dd HH:mm:ss); } export function timeFormat(date) { return formatDate(date, HH:mm:ss); } export default formatDate;调用方式formatDate(new Date(), yyyy-MM-dd); // 2026-07-01 formatDate(Date.now(), HH:mm:ss); // 16:02:34 formatDate(2026-07-01, yyyy年MM月dd日); // 2026年07月01日 // 快捷方法 dateFormat(new Date()); // 2026-07-01 datetimeFormat(new Date()); // 2026-07-01 16:02:34 timeFormat(new Date()); // 16:02:343、安全解析 JSONsafeJsonParse/** * 安全解析 JSON解析失败时返回 defaultValue 而不抛异常 * param {string} str - 待解析的 JSON 字符串 * param {*} defaultValue - 解析失败时的默认返回值默认 null * returns {*} 解析结果或默认值 */ export function safeJsonParse(str, defaultValue null) { try { return JSON.parse(str); } catch { return defaultValue; } } export default safeJsonParse;调用方式safeJsonParse({a:1}); // { a: 1 } safeJsonParse({坏数据}, {}); // {}返回默认值不抛异常 safeJsonParse(xxx); // null4、判断空对象isEmpty/** * 判断是否为空对象自身可枚举属性个数为 0 * param {Object} obj * returns {boolean} */ export function isEmpty(obj) { return Object.keys(obj).length 0; } export default isEmpty;调用方式isEmpty({}); // true isEmpty({ a: 1 }); // false5、复制到剪贴板copyToClipboard/** * 复制文本到剪贴板 * param {string} text - 要复制的文本 * returns {Promiseboolean} 是否复制成功 */ export async function copyToClipboard(text) { // 优先使用现代 Clipboard API if (navigator.clipboard navigator.clipboard.writeText) { try { await navigator.clipboard.writeText(text); return true; } catch { // 降级到 fallback } } // fallback使用 textarea execCommand const textarea document.createElement(textarea); textarea.value text; textarea.style.position fixed; textarea.style.left -9999px; textarea.style.top -9999px; document.body.appendChild(textarea); textarea.select(); let success false; try { success document.execCommand(copy); } catch { // ignore } document.body.removeChild(textarea); return success; } export default copyToClipboard;调用方式// 异步调用返回布尔值表示成功与否 const ok await copyToClipboard(Hello World); // 或者配合按钮点击 btn.onclick async () { await copyToClipboard(已复制); };6、延时工具 重试工具/** * 延时工具返回一个在指定毫秒后 resolve 的 Promise * param {number} ms - 等待毫秒数 * returns {Promisevoid} */ export function sleep(ms) { return new Promise((resolve) setTimeout(resolve, ms)); } /** * 重试工具fn 失败时自动重试 * param {Function} fn - 返回 Promise 的异步函数 * param {number} times - 最大重试次数默认 3 * param {number} interval - 重试间隔毫秒默认 1000 * returns {Promise*} fn 成功时的返回值 */ export async function retry(fn, times 3, interval 1000) { let lastError; for (let i 0; i times; i) { try { return await fn(); } catch (err) { lastError err; if (i times) { await sleep(interval); } } } throw lastError; } export default { sleep, retry };调用方式// 延时 2 秒 await sleep(2000); // 重试最多 3 次每次间隔 1 秒 const data await retry(() fetch(/api/data).then((r) r.json()), 3, 1000);7、数组工具uniqueArray 更多/** * 数组去重 */ export function uniqueArray(arr) { return [...new Set(arr)]; } /** * 按指定 key 去重保留第一个出现的元素 * param {Array} arr * param {string} key */ export function uniqueBy(arr, key) { const seen new Set(); return arr.filter((item) { const val item[key]; if (seen.has(val)) return false; seen.add(val); return true; }); } /** * 将数组按 size 拆分返回二维数组 * param {Array} arr * param {number} size - 每组的长度 */ export function chunk(arr, size) { if (size 0) return []; const result []; for (let i 0; i arr.length; i size) { result.push(arr.slice(i, i size)); } return result; } /** * 按指定 key 排序不改变原数组 * param {Array} arr * param {string} key * param {string} order - asc 升序 / desc 降序默认 asc */ export function sortBy(arr, key, order asc) { return [...arr].sort((a, b) { // null / undefined 排在末尾 const left a[key] ?? ; const right b[key] ?? ; if (left right) return order asc ? -1 : 1; if (left right) return order asc ? 1 : -1; return 0; }); }调用方式// uniqueArray uniqueArray([1, 2, 2, 3, 3]); // → [1, 2, 3] // uniqueBy const users [ { id: 1, name: 张三 }, { id: 2, name: 李四 }, { id: 1, name: 张三 }, ]; uniqueBy(users, id); // → [{ id: 1, name: 张三 }, { id: 2, name: 李四 }] // chunk chunk([1, 2, 3, 4, 5, 6, 7], 3); // → [[1, 2, 3], [4, 5, 6], [7]] // sortBy const list [ { name: 张三, age: 28 }, { name: 李四, age: 22 }, { name: 王五, age: 35 }, ]; sortBy(list, age, asc); // → [{ name: 李四, age: 22 }, { name: 张三, age: 28 }, { name: 王五, age: 35 }] sortBy(list, age, desc); // → [{ name: 王五, age: 35 }, { name: 张三, age: 28 }, { name: 李四, age: 22 }]8、文件下载downloadFile/** * 通过 URL 下载文件 * param {string} url - 文件地址 * param {string} filename - 文件名 */ export function downloadFile(url, filename) { const a document.createElement(a); a.href url; a.download filename; a.style.display none; document.body.appendChild(a); a.click(); document.body.removeChild(a); } /** * 下载 Blob * param {Blob} blob * param {string} filename */ export function downloadBlob(blob, filename) { const url URL.createObjectURL(blob); downloadFile(url, filename); // 延迟释放确保浏览器已开始下载 setTimeout(() URL.revokeObjectURL(url), 1000); } /** * 下载文本内容 * param {string} content - 文本内容 * param {string} filename * param {string} mimeType - MIME 类型默认 text/plain;charsetutf-8 */ export function downloadText(content, filename, mimeType text/plain;charsetutf-8) { const blob new Blob([content], { type: mimeType }); downloadBlob(blob, filename); } export default { downloadFile, downloadBlob, downloadText };调用方式// 直接下链接 downloadFile(https://example.com/report.pdf, report.pdf); // 二进制 Blob const blob await fetch(/api/export).then((r) r.blob()); downloadBlob(blob, export.xlsx); // 文本 downloadText(JSON.stringify({ a: 1 }, null, 2), data.json, application/json;charsetutf-8);9、深拷贝deepClone/** * 深拷贝支持对象、数组、Date、RegExp、Map、Set * param {*} source - 源数据 * param {WeakMap} [hash] - 内部使用处理循环引用 * returns {*} 拷贝结果 */ export function deepClone(source, hash new WeakMap()) { // null 或非对象直接返回 if (source null || typeof source ! object) return source; // 循环引用已拷贝过则直接取缓存 if (hash.has(source)) return hash.get(source); // Date if (source instanceof Date) return new Date(source); // RegExp if (source instanceof RegExp) return new RegExp(source.source, source.flags); // Map if (source instanceof Map) { const copy new Map(); hash.set(source, copy); source.forEach((val, key) copy.set(deepClone(key, hash), deepClone(val, hash))); return copy; } // Set if (source instanceof Set) { const copy new Set(); hash.set(source, copy); source.forEach((val) copy.add(deepClone(val, hash))); return copy; } // Array / Object const copy Array.isArray(source) ? [] : {}; hash.set(source, copy); for (const key of Reflect.ownKeys(source)) { copy[key] deepClone(source[key], hash); } return copy; } export default deepClone;调用方式const obj { a: 1, b: { c: 2 }, d: [1, 2, 3] }; const copy deepClone(obj); copy.b.c 999; obj.b.c; // 2不受影响 // 循环引用也安全 const circular { self: null }; circular.self circular; deepClone(circular); // 不爆栈10、URL 参数/** * 解析 URL 查询参数返回 key-value 对象 * param {string} [url] - URL默认取当前页面地址 * returns {Object} */ export function getUrlParams(url) { const query url ? new URL(url).searchParams : location.search ? new URLSearchParams(location.search) : new URLSearchParams(); const params {}; for (const [key, value] of query) { // 同名参数转数组 if (key in params) { params[key] [].concat(params[key], value); } else { params[key] value; } } return params; } /** * 获取单个查询参数 * param {string} name * param {string} [url] * returns {string|null} */ export function getUrlParam(name, url) { const query url ? new URL(url).searchParams : new URLSearchParams(location.search); return query.get(name); } export default { getUrlParams, getUrlParam };调用方式// 当前页面: ?a1b2b3 getUrlParams(); // → { a: 1, b: [2, 3] } getUrlParam(a); // → 1 // 指定 URL getUrlParams(https://a.com?namefoopage1); // → { name: foo, page: 1 }统一导出index.jsexport { debounce } from ./debounce.js; export { formatDate, dateFormat, datetimeFormat, timeFormat } from ./formatDate.js; export { safeJsonParse } from ./safeJsonParse.js; export { isEmpty } from ./isEmpty.js; export { copyToClipboard } from ./copyToClipboard.js; export { sleep, retry } from ./async.js; export { uniqueArray, uniqueBy, chunk, sortBy } from ./array.js; export { downloadFile, downloadBlob, downloadText } from ./download.js; export { deepClone } from ./deep-clone.js; export { getUrlParams, getUrlParam } from ./url.js;