记录几个前端实用小函数

首先建好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';
相关推荐
Lank_M几秒前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写2 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李4 分钟前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易18 分钟前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding22 分钟前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马29 分钟前
前沿视界
前端
kyriewen33 分钟前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒1 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端
程序员大阳2 小时前
HTML+CSS+JS对话式实战(9)-项目总结与问题分析
javascript·css·html·实战·项目总结