zpy-tools
Version:
提供了日期格式化、日期计算、文件格式化、本地缓存处理、Base64转File、File转Base64、节流防抖、数组排序、数据结构转化、深度比较两个对象是否包含相同的值等相关功能
690 lines (594 loc) • 19.7 kB
JavaScript
/**
* 获取数据类型
* @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,
};