UNPKG

zpy-tools

Version:

提供了日期格式化、日期计算、文件格式化、本地缓存处理、Base64转File、File转Base64、节流防抖、数组排序、数据结构转化、Markdown 转为适合微信分享的纯文本、深度比较两个对象是否包含相同的值等相关功能

690 lines (594 loc) 19.7 kB
/** * 获取数据类型 * @param {*} value - 值 * @returns {String} 数据类型 */ function getDataType(value) { // 特殊情况处理 if (value === null) return "null"; if (value === undefined) return "undefined"; // 使用 Object.prototype.toString 进行类型检测 const toString = Object.prototype.toString; const typeMap = { boolean: "[object Boolean]", number: "[object Number]", string: "[object String]", function: "[object Function]", array: "[object Array]", date: "[object Date]", regexp: "[object RegExp]", object: "[object Object]", }; const typeStr = toString.call(value); for (let [key, val] of Object.entries(typeMap)) { if (typeStr === val) { return key; } } // 如果是未定义的类型,则返回 'unknown' return "unknown"; } /** * 对象深拷贝 * @param {*} data - 值 * @returns {*} 拷贝后的值 */ function deepClone(data, seen = new WeakMap()) { // 参数验证 if (data === null || typeof data !== "object") { return data; } // 处理循环引用 if (seen.has(data)) { return seen.get(data); } let clone; // 特殊对象类型的处理 if (data instanceof Date) { return new Date(data); } if (data instanceof RegExp) { return new RegExp(data); } // 初始化克隆对象 if (Array.isArray(data)) { clone = []; } else { clone = {}; } // 记录已处理的对象,防止循环引用 seen.set(data, clone); // 深拷贝对象属性或数组元素 for (let key in data) { if (data.hasOwnProperty(key)) { clone[key] = deepClone(data[key], seen); } } // 深拷贝数组元素 if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { clone[i] = deepClone(data[i], seen); } } return clone; } /** * 将数组拼接成字符串 * @param {*} value - 值,可以是数组或字符串 * @param {String} [separator=","] - 分隔符,默认为逗号 * @returns {String} 拼接后的字符串 */ function arrayToString(value, separator = ",") { // 参数验证 if (typeof separator !== "string") { throw new TypeError("The separator must be a string."); } // 处理 value 为空的情况 if (value == null) { return ""; } // 如果 value 已经是字符串,直接返回 if (typeof value === "string") { return value; } // 确保 value 是数组 if (!Array.isArray(value)) { throw new TypeError("The value must be an array or a string."); } // 如果数组为空,返回空字符串 if (value.length === 0) { return ""; } // 拼接数组为字符串 return value.join(separator); } /** * 将字符串拆分成数组 * @param {*} value - 值,可以是字符串或数组 * @param {String} [separator=","] - 分隔符,默认为逗号 * @returns {Array} 拆分后的数组 */ function stringToArray(value, separator = ",") { // 参数验证 if (typeof separator !== "string") { throw new TypeError("The separator must be a string."); } // 处理 value 为空的情况 if (value == null || value === "") { return []; } // 如果 value 已经是一个数组,直接返回 if (Array.isArray(value)) { return value; } // 确保 value 是字符串 if (typeof value !== "string") { throw new TypeError("The value must be a string or an array."); } // 拆分字符串为数组 return value .split(separator) .map((item) => item.trim()) .filter(Boolean); } /** * 将扁平数组转换为树形结构 * @param {Array} items - 扁平数组,每个元素包含 id 和 parentId 属性 * @param {String} [idKey='id'] - 表示唯一标识的键名,默认为 'id' * @param {String} [parentIdKey='parentId'] - 表示父级标识的键名,默认为 'parentId' * @param {*} [rootValue=null] - 根节点的父级标识值,默认为 null * @returns {Array} 转换后的树形结构数组 */ function arrayToTree( items, idKey = "id", parentIdKey = "parentId", rootValue = null ) { // 参数验证 if (!Array.isArray(items)) { throw new TypeError("The first argument must be an array."); } if (typeof idKey !== "string") { throw new TypeError("The idKey must be a string."); } if (typeof parentIdKey !== "string") { throw new TypeError("The parentIdKey must be a string."); } const tree = []; const lookup = {}; // 用于快速查找节点 // 首先创建所有节点的查找表 items.forEach((item) => { lookup[item[idKey]] = { ...item, children: [] }; }); // 构建树形结构 items.forEach((item) => { const node = lookup[item[idKey]]; const parent = lookup[item[parentIdKey]]; if (parent) { // 如果找到了父节点,则将当前节点添加到父节点的 children 中 parent.children.push(node); } else if (item[parentIdKey] === rootValue) { // 如果没有找到父节点,并且当前节点是根节点,则添加到树中 tree.push(node); } }); return tree; } /** * 将树形结构转换为数组 * @param {Object} tree - 树形结构的根节点 * @param {Array} [result=[]] - 用于收集结果的数组,可选,默认为空数组 * @returns {Array} 转换后的数组 */ function treeToArray(tree, result = []) { // 检查 tree 是否是对象且不是 null 或 undefined if (typeof tree !== "object" || tree === null || tree === undefined) { throw new TypeError("The first argument must be a non-null object."); } // 确保 result 是一个数组 if (!Array.isArray(result)) { throw new TypeError("The second argument must be an array."); } // 使用递归函数来避免污染默认参数 function traverse(node) { result.push(node); if (node.children && Array.isArray(node.children)) { node.children.forEach((child) => traverse(child)); } } traverse(tree); return result; } /** * 获取指定节点的父节点的 key 值 * @param {Array} tree - 树形结构数组 * @param {String} value - 目标叶子节点的 key 值 * @param {Array} currentPath - 当前路径,用于递归调用 * @returns {Array} 父节点的 key 值数组,如果没有找到,则返回 null */ function getParentKeys(tree, value, valueKey = 'id', childrenKey = 'children', currentPath = []) { // 遍历树中的每一个节点 for (const node of tree) { // 如果找到了目标叶子节点,则返回当前路径加上该节点的 key if (node[valueKey] === value) { return [...currentPath]; } // 如果节点有子节点,则递归查找 if (node[childrenKey] && node[childrenKey].length > 0) { // 将当前节点的 valueKey 添加到路径中 const pathWithCurrentNode = [...currentPath, node[valueKey]]; // 递归调用以查找子节点中的目标叶子节点 const result = getParentKeys( node[childrenKey], value, valueKey, childrenKey, pathWithCurrentNode ); // 如果在子节点中找到了目标叶子节点,则返回结果 if (result) { return result; } } } // 如果没有找到目标叶子节点,则返回 null 或者空数组 return null; } /** * 数组排序 * 对一维数组或二维数组(对象数组)进行排序。 * 根据指定的键对对象数组进行排序,并且可以处理混合了数字和字符串的值,确保数字部分被正确解析和比较。 * 此外,该函数支持正序和倒序排序,并允许用户通过提供 locales 和 compareOptions 来定制字符串比较的行为。 * @param {Array} arr - 要排序的数组 * @param {Object} options - 排序选项 * key - 排序的键名,默认为空 * isDesc - 是否降序,默认为false * locales - 比较时使用的本地化信息,默认为'zh' * ...compareOptions - 其他用于比较的选项,例如sensitivity等 * @returns {Array} 排序后的数组 */ function sortArray(arr, options = {}) { const defaultOptions = { key: undefined, isDesc: false, locales: "zh", // 使用中文作为默认语言环境 sensitivity: "base", // 忽略大小写和变音符号等 }; // 合并默认选项与用户提供的选项 const { key, isDesc, locales, ...compareOptions } = Object.assign( {}, defaultOptions, options ); // 输入验证 if (!Array.isArray(arr)) { throw new Error("The first argument must be an array."); } if (typeof key !== "string" && typeof key !== "undefined") { throw new Error( 'The "key" option must be a string or undefined for one-dimensional arrays.' ); } // 如果是二维数组或者对象数组,则使用key,否则直接比较元素 const getValue = (item) => (typeof item === "object" ? item[key] : item); // 确定排序顺序 const sortOrder = isDesc ? -1 : 1; // 解析所有项,避免每次比较都重新解析 const parsedArr = arr.map((item) => ({ original: item, components: extractTextComponents(String(getValue(item))), })); let result; return parsedArr .sort((a, b) => { const parsedA = a.components; const parsedB = b.components; // 比较规则:先按中文,再按字母,最后按数字 result = sortOrder * compareStrings( parsedA.chinese.join(""), parsedB.chinese.join(""), locales, compareOptions ); if (result !== 0) return result; result = sortOrder * compareStrings( parsedA.letters.join(""), parsedB.letters.join(""), locales, compareOptions ); if (result !== 0) return result; return sortOrder * (parsedA.numbers - parsedB.numbers); }) .map((item) => item.original); // 返回原始元素组成的数组 } /** * 辅助函数:解析值为中文、字母、数字三部分 * @param {string} value - 待处理的值 * @returns {{chinese: Array, letters: Array, numbers: Number}} - 分离出的中文、字母和数字部分 */ function extractTextComponents(value) { // 参数校验:确保输入是字符串或可转换为字符串 if (value == null) { return { chinese: [], letters: [], numbers: 0 }; } const strValue = String(value); const chinese = []; const letters = []; let numbers = 0; let numBuffer = ""; let charCode = ""; for (let i = 0; i < strValue.length; i++) { charCode = strValue.charCodeAt(i); if (charCode >= 0x4e00 && charCode <= 0x9fff) { chinese.push(strValue[i]); } else if ( (charCode >= 65 && charCode <= 90) || (charCode >= 97 && charCode <= 122) ) { letters.push(strValue[i]); } else if (charCode >= 48 && charCode <= 57) { numBuffer += strValue[i]; } } // 将累积的数字字符串转换为整数 if (numBuffer) { numbers = parseInt(numBuffer, 10); } return { chinese, letters, numbers }; } /** * 辅助函数:比较两个字符串,考虑本地化设置 * @param {string} strA - 第一个字符串 * @param {string} strB - 第二个字符串 * @param {string} locales - 本地化设置 * @param {Object} compareOptions - 比较选项 * @returns {number} - 比较结果 */ function compareStrings(strA, strB, locales, compareOptions) { // Fallback to simple comparison for IE which doesn't support all localeCompare options if ( !("localeCompare" in String.prototype) || !String.prototype.localeCompare.length ) { return strA.toLowerCase().localeCompare(strB.toLowerCase()); } return strA.localeCompare(strB, locales, compareOptions); } /** * 深度比较两个对象是否包含相同的值 * @param {Object} obj1 - 第一个对象 * @param {Object} obj2 - 第二个对象 * @returns {Boolean} - 如果两个对象的值完全相同,返回true,否则返回false */ function hasObjectsEqual(obj1, obj2, seenPairs = new WeakMap()) { // 检查是否是相同的引用 if (obj1 === obj2) return true; // 检查是否至少有一个参数不是对象或为 null/undefined if ( obj1 == null || // 包括 null 和 undefined obj2 == null || typeof obj1 !== "object" || typeof obj2 !== "object" ) { return obj1 === obj2; } // 检查是否已经是比较过的对象对 if (seenPairs.has(obj1)) { return seenPairs.get(obj1) === obj2; } // 处理特殊对象类型(如 Date) if (obj1.constructor !== obj2.constructor) return false; // 特殊处理数组 if (Array.isArray(obj1)) { if (!Array.isArray(obj2) || obj1.length !== obj2.length) return false; for (let i = 0; i < obj1.length; i++) { if (!hasObjectsEqual(obj1[i], obj2[i], seenPairs)) return false; } return true; } // 记录已经比较过的对象对,防止循环引用导致的死循环 seenPairs.set(obj1, obj2); const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; for (let key of keys1) { if ( !Object.prototype.hasOwnProperty.call(obj2, key) || !hasObjectsEqual(obj1[key], obj2[key], seenPairs) ) { return false; } } return true; } /** * 防抖函数工厂 * @param {Function} fn - 需要执行的函数 * @param {Number} [delay=500] - 延迟时间,默认500ms * @param {Boolean} [immediate=false] - 是否立即执行,默认false * @returns {Function} 返回防抖包装后的函数 */ function debounce(fn, delay = 500, immediate = false) { // 检查 fn 是否是函数 if (typeof fn !== "function") { throw new TypeError("The first argument must be a function."); } // 检查 delay 是否是非负数 if (typeof delay !== "number" || delay < 0) { delay = 500; // 使用默认值 } let timeoutId; let context; let args; // 返回防抖包装后的函数 return function (...callArgs) { context = this; args = callArgs; // 如果存在定时器,说明有新的调用,取消之前的调用计划 if (timeoutId) clearTimeout(timeoutId); // 如果设置了 immediate 并且这是第一次调用,则立即执行 fn if (immediate && !timeoutId) { fn.apply(context, args); } // 设置新的定时器,如果在指定时间内函数未再次被调用,将执行fn timeoutId = setTimeout(() => { if (!immediate) { fn.apply(context, args); } timeoutId = null; }, delay); }; } /** * 节流函数工厂 * @param {Function} fn - 需要执行的函数 * @param {Number} [interval=1000] - 间隔时间,默认1000ms * @returns {Function} 返回节流包装后的函数 */ function throttle(fn, interval = 1000) { // 检查 fn 是否是函数 if (typeof fn !== "function") { throw new TypeError("The first argument must be a function."); } // 检查 interval 是否是正数 if (typeof interval !== "number" || interval < 0) { interval = 1000; // 使用默认值 } let lastExecution = 0; // 返回节流包装后的函数 return function (...args) { const now = Date.now(); const context = this; // 如果上一次执行是在 interval 之前,或者这是第一次执行 if (now - lastExecution >= interval) { fn.apply(context, args); // 更新上一次执行时间 lastExecution = now; } }; } /** * 获取URL中的参数 * @param {String} [url] - URL字符串,默认为当前页面的URL * @returns {Object} 包含URL参数的对象 */ function getUrlParams(url) { // 检查 url 是否是有效的字符串 if (typeof url !== "string" || url.trim() === "") { url = window.location.href; } else { try { // 验证是否为合法的URL new URL(url); } catch (e) { throw new TypeError("Invalid URL provided."); } } const params = {}; // 解析URL中的查询字符串 const queryString = new URL(url).search.slice(1); if (queryString) { // 使用 URLSearchParams 来处理查询字符串 const searchParams = new URLSearchParams(queryString); for (const [key, value] of searchParams.entries()) { // 如果对象中已经存在该键,则将值转换为数组存储 if (params.hasOwnProperty(key)) { if (Array.isArray(params[key])) { params[key].push(value); } else { params[key] = [params[key], value]; } } else { params[key] = value; } } } return params; } /** * 将参数添加到URL * @param {string} url 基础URL * @param {Object} params 要添加的参数对象 * @returns {string} 添加了参数后的URL */ function addParamsToUrl(url, params) { // 检查 url 是否是字符串且不为空 if (typeof url !== "string" || url.trim() === "") { throw new TypeError("The URL must be a non-empty string."); } // 检查 params 是否是一个对象(非数组) if (typeof params !== "object" || Array.isArray(params) || params === null) { throw new TypeError( "The params must be an object and not an array or null." ); } // 过滤掉值为 undefined 或者不是有效的字符串、数字、布尔值或对象的键值对 const filteredParams = Object.entries(params).filter( ([, value]) => value !== undefined && (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date) ); // 如果没有有效的参数,则直接返回原始URL if (filteredParams.length === 0) return url; // 将过滤后的对象转换为查询字符串 const paramsStr = filteredParams .map( ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}` ) .join("&"); // 拼接到URL后面 const separator = url.includes("?") ? "&" : "?"; return `${url}${separator}${paramsStr}`; } /** * 将数组按给定的属性进行分类 * @param {Array} array 需要分类的数组 * @param {String} property 用于分类的属性名 * @returns {Object} 分类结果,键为属性值,值为对应的数组 */ function classifyBy(array, property) { // 检查 array 是否是一个数组 if (!Array.isArray(array)) { throw new TypeError("The first argument must be an array."); } // 检查 property 是否是字符串或数字(属性名可以是字符串或数字) if (typeof property !== "string" && typeof property !== "number") { throw new TypeError( "The second argument must be a string or number representing the property name." ); } return array.reduce((accumulator, obj) => { const key = obj[property]; if (!accumulator[key]) { accumulator[key] = []; } accumulator[key].push(obj); return accumulator; }, {}); } module.exports = { getDataType, deepClone, arrayToString, stringToArray, arrayToTree, treeToArray, getParentKeys, sortArray, hasObjectsEqual, debounce, throttle, getUrlParams, addParamsToUrl, classifyBy, };