首先建好utils目录结构
写任何业务代码之前,都要先把工具函数库的骨架搭好
javascript
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、防抖(Debounce)
Debounce应该是用得最多的一个,没有之一。搜索框、滚动监听、窗口 resize,任何"用户操作会高频触发"的场景都需要它。
javascript
/**
* 防抖函数
* 在停止调用 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;
调用方式
javascript
// 搜索框输入防抖:停止输入 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)
javascript
/**
* 格式化日期
* @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;
调用方式
javascript
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:34
3、安全解析 JSON(safeJsonParse)
javascript
/**
* 安全解析 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;
调用方式
javascript
safeJsonParse('{"a":1}'); // { a: 1 }
safeJsonParse('{坏数据}', {}); // {}(返回默认值,不抛异常)
safeJsonParse('xxx'); // null
4、判断空对象(isEmpty)
javascript
/**
* 判断是否为空对象(自身可枚举属性个数为 0)
* @param {Object} obj
* @returns {boolean}
*/
export function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
export default isEmpty;
调用方式
javascript
isEmpty({}); // true
isEmpty({ a: 1 }); // false
5、复制到剪贴板(copyToClipboard)
javascript
/**
* 复制文本到剪贴板
* @param {string} text - 要复制的文本
* @returns {Promise<boolean>} 是否复制成功
*/
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;
调用方式
javascript
// 异步调用,返回布尔值表示成功与否
const ok = await copyToClipboard('Hello World');
// 或者配合按钮点击
btn.onclick = async () => {
await copyToClipboard('已复制!');
};
6、延时工具 & 重试工具
javascript
/**
* 延时工具,返回一个在指定毫秒后 resolve 的 Promise
* @param {number} ms - 等待毫秒数
* @returns {Promise<void>}
*/
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 };
调用方式
javascript
// 延时 2 秒
await sleep(2000);
// 重试最多 3 次,每次间隔 1 秒
const data = await retry(() => fetch('/api/data').then((r) => r.json()), 3, 1000);
7、数组工具(uniqueArray + 更多)
javascript
/**
* 数组去重
*/
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;
});
}
调用方式
javascript
// ===== 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)
javascript
/**
* 通过 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;charset=utf-8')
*/
export function downloadText(content, filename, mimeType = 'text/plain;charset=utf-8') {
const blob = new Blob([content], { type: mimeType });
downloadBlob(blob, filename);
}
export default { downloadFile, downloadBlob, downloadText };
调用方式
javascript
// 直接下链接
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;charset=utf-8');
9、深拷贝(deepClone)
javascript
/**
* 深拷贝(支持对象、数组、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;
调用方式
javascript
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 参数
javascript
/**
* 解析 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 };
调用方式
javascript
// 当前页面: ?a=1&b=2&b=3
getUrlParams();
// → { a: '1', b: ['2', '3'] }
getUrlParam('a');
// → '1'
// 指定 URL
getUrlParams('https://a.com?name=foo&page=1');
// → { name: 'foo', page: '1' }
统一导出(index.js)
javascript
export { 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';