记录几个前端实用小函数

首先建好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';
相关推荐
IDIOT___IDIOT1 小时前
Windows 解锁后 Chrome 弹出白屏的排查与修复
前端·chrome
tuddy7894641 小时前
前端应用的离线暂停更新策略:原理、实现与最佳实践
前端
希冀1232 小时前
【JavaScript】Javascript—JS进阶—Day03
开发语言·javascript·原型模式
hunterandroid2 小时前
多线程与线程池:从 Thread 到高效并发
前端
光影少年2 小时前
RN 架构:JS层、原生桥接层、原生渲染层 通信原理
开发语言·javascript·架构
用户69371750013842 小时前
AI Agent 里的 Loop 到底是什么?
android·前端·后端
甜美的小天鹅2 小时前
TDD in HTML & JavaScript 概述
javascript·html·tdd
竹林8182 小时前
用 Viem 替代 ethers.js 重写 DeFi 前端:从踩坑 3 天到丝滑体验,我总结了这些关键差异
javascript
hunterandroid2 小时前
内存泄漏与 LeakCanary:从问题定位到修复思路
前端