UNPKG

@mxssfd/ts-utils

Version:
1,715 lines (1,710 loc) 191 kB
/*! * tsUtils(@mxssfd/ts-utils) v5.0.1-beta.1 * Author: dyh * Documentation: https://github.com/mengxinssfd/ts-utils#readme * Date: 2022-11-8 */ // 获取object树的最大层数 tree是object的话,tree就是层数1 function getTreeMaxDeep(tree) { function deeps(obj, num = 0) { if (typeof tree !== 'object' || tree === null) return num; const arr = [++num]; forEachObj(obj, (v) => { arr.push(deeps(v, num)); }); return Math.max(...arr); } return deeps(tree); } // 获取树某层的节点数 0是tree本身 function getTreeNodeLen(tree, nodeNumber = 1) { let result = 0; if (typeof tree !== 'object' || tree === null || nodeNumber < 0) return result; function deeps(obj, num = 0) { if (nodeNumber === num++) { result++; return; } forEachObj(obj, (v) => { deeps(v, num); }); } deeps(tree); return result; } // 合并两个object TODO 可优化 function deepMerge(first, second) { function assign(receive, obj) { for (const k in obj) { if (!hasOwn(obj, k)) continue; const v = obj[k]; if (v && typeof v === 'object') { receive[k] = new v.constructor(); assign(receive[k], v); } else receive[k] = v; } } const result = {}; assign(result, first); assign(result, second); return result; } /** * 代替Object.keys(obj).forEach,减少循环次数 * @param obj * @param callbackFn 返回false的时候中断 * @param elseCB 遍历完后执行 * @returns {boolean} isDone */ function forEachObj(obj, callbackFn, elseCB) { for (const k in obj) { if (!hasOwn(obj, k)) continue; const v = obj[k]; if (callbackFn(v, k, obj) === false) return false; } elseCB && elseCB(); return true; } /** * @alias forEachObj */ const objForEach = forEachObj; /** * object key-value翻转 * @param obj */ function getReverseObj(obj) { return reduceObj(obj, (res, v, k) => { res[v] = k; return res; }, {}); } /** * 代替Object.keys(obj).reduce,减少循环次数 * @param obj * @param callbackFn * @param initialValue 初始值 数组中可以省略,默认使用数组中的第一项作为初始值,但object不存在第一项,所以不能省略 */ function reduceObj(obj, callbackFn, initialValue) { let result = initialValue; forEachObj(obj, (v, k, o) => { result = callbackFn(result, v, k, o); }); return result; } /** * @alias reduceObj */ const objReduce = reduceObj; /** * @param originObj * @param pickKeys * @param cb */ function pickByKeys(originObj, pickKeys, cb) { const callback = cb || ((v) => v); return pickKeys.reduce((res, key) => { if (hasOwn(originObj, key)) res[key] = callback(originObj[key], key, originObj); return res; }, {}); } // TODO 不完美的地方:k === "a"时应该限定返回值类型为number /*pickByKeys({a: 123, b: "111", c: false}, ["a", "b"], (v, k, o) => { if(k === "a"){ return "123123" } return v; });*/ // 新属性名作为键名的好处是可以多个属性对应一个值 function pickRename(originObj, pickKeyMap, cb) { const callback = cb || ((v) => v); /* const renames = Object.keys(renamePickObj) as (keyof O)[]; return renames.reduce((result, rename) => { const pick = renamePickObj[rename]; if (originObj.hasOwnProperty(pick)) { result[rename] = callback(originObj[pick], pick, originObj); } return result; }, {} as any);*/ return reduceObj(pickKeyMap, (result, pick, rename) => { if (hasOwn(originObj, pick)) { result[rename] = callback(originObj[pick], pick, originObj); } return result; }, {}); } /** * 合并pickByKeys与pickRename两者的功能 */ function pick(originObj, picks, cb) { const isObj = isObject(picks); // ------- 第一种写法 ------- // const callback = cb || (v => v); // const pickKeys = isObj ? Object.keys(picks) : picks; // const getOriginObjKey = isObj ? k => picks[k] : k => k; // return pickKeys.reduce((res, k) => { // const originObjKey = getOriginObjKey(k); // if (originObj.hasOwnProperty(originObjKey)) { // res[k] = callback(originObj[originObjKey], originObjKey, originObj); // } // return res; // }, {} as any); // ------- 第二种写法 ------- // 更简洁 减少判断次数 // TODO 需要判断返回值类型是否改变了 改变则抛出异常 return isObj ? pickRename(originObj, picks, cb) : pickByKeys(originObj, picks, cb); } // pick({a: 132, b: "123123"}, ["a", "b"]); /** * 从其他对象中挑出与原对象值不一样的或原对象中不存在的键值对所组成的新对象 * @param origin * @param objs * @param verify */ function pickDiff(origin, objs, verify) { const verifyFn = verify || ((originV, objV, k, origin) => { return (hasOwn(origin, k) && originV === objV) || (isNaN(originV) && isNaN(objV)); }); return objs.reduce((result, obj) => { objForEach(obj, (v, k) => { if (verifyFn(origin[k], v, k, origin, obj)) return; result[k] = v; }); return result; }, {}); } /** * 从其他对象中挑出与原对象中不存在的键值对所组成的新对象 */ function pickAdditional(origin, ...others) { // return others.reduce((result, obj) => { // objForEach(obj, (v, k) => { // if (!origin.hasOwnProperty(k)) // result[k] = v; // }); // return result; // }, {}); return pickDiff(origin, others, (_originV, _objV, k) => hasOwn(origin, k)); } /** * 根据新键值对重命名对象的key,并生成一个新的对象 * @param originObj * @param keyMap */ function renameObjKey(originObj, keyMap) { const result = assign({}, originObj); const delKeys = []; const newKeys = []; forEachObj(keyMap, (originKey, k) => { if (hasOwn(result, originKey)) { result[k] = result[originKey]; delKeys.push(originKey); newKeys.push(k); } }); // 可能新key会与旧key同名,如果是同名则把该key从要删除的key数组中移除 // delKeys = delKeys.filter(k => newKeys.indexOf(k as string) === -1); delKeys.forEach((k) => { if (newKeys.indexOf(k) > -1) return; delete result[k]; }); return result; } /** * Omit 省略 * @example * // returns {c: true} * omit({a: 123, b: "bbb", c: true}, ["a", "b"]) * @param target * @param keys */ function omit(target, keys) { const newKeys = keys.slice(); return reduceObj(target, (initValue, v, k) => { const index = newKeys.indexOf(k); if (index === -1) { initValue[k] = v; } else { newKeys.splice(index, 1); } return initValue; }, {}); } function assign(target, ...args) { args.forEach((arg) => { // forEachObj(arg, (v, k) => target[k] = v); // 不能返回“target[k] = v”值,v可能会为false,为false会中断循环 forEachObj(arg, (v, k) => { target[k] = v; }); }); return target; } /** * 与lodash defaults一样 只替换target里面的值为undefined的属性 * 类型推导会以前面的为准 * @example * defaults({a: 12, b: undefined, c: 3}, {a: 1}, {b: 2}, {d: 4}); // returns {a: 12, b: 2, c: 3, d: 4} * defaults({a:12,b:undefined,c:3},{a:1},{b:2},{c:undefined}); // returns {a: 12, b: 2, c: 3} * @param target * @param args */ function defaults(target, ...args) { args.forEach((arg) => { forEachObj(arg, (v, k) => { if (v === undefined || target[k] !== undefined) return; target[k] = v; }); }); return target; } /** * 内部更新对象函数 */ function _updateObj(target, others, k) { // 从后往前查起 forEachRight(others, (item) => { if (item && hasOwn(item, k)) { target[k] = item[k]; // 某个对象有这个key,那么就不再往前查 return false; } }); } /** * 使用target里面的key去查找其他的对象,如果其他对象里有该key,则把该值赋给target,如果多个对象都有同一个值,则以最后的为准 * 会更新原对象 * * 如果要更新某个class的实例,那么需要使用updateIns * @see updateIns * @alias updateObj * * @param target * @param others */ function objUpdate(target, ...others) { if (others[others.length - 1] === target) return target; objForEach(target, (_v, k) => _updateObj(target, others, k)); return target; } // export function updateObj<T extends object>(target: T, ...others: object[]): T { // for (const k in target) { // for (let i = others.length - 1; i > -1; i--) { // const item = others[i]; // if (item && hasOwn(item, k)) { // target[k] = item[k]; // break; // } // } // } // // return target; // } /** * @see objUpdate * @alias objUpdate */ const updateObj = objUpdate; /** * 获取class实例的key数组 * @param ins */ function getInsKeys(ins) { const result = []; let cur = ins; while (cur) { // 普通key result.push(...Object.keys(cur)); // 使用 Object.getPrototypeOf 代替 cur.__proto__ const proto = Object.getPrototypeOf(cur); if (!proto || proto === Object.prototype) { // 构造器为原生函数的话就不是class // 或者对象原型和 Object 的原型一样则不是class // 或者 Object.create(null) 创建的纯对象,prototype为null, __proto__为undefined break; } else { // method key result.push(...Reflect.ownKeys(proto)); } cur = proto; } // 过滤掉构造方法,并去重 return unique(result.filter((k) => k !== 'constructor')); } /** * 更新实例对象属性 * * updateIns可以代替updateObj使用, * 不过由于遍历了实例及原型的key,所以理论上updateIns会比updateObj慢一点 * * @see objUpdate */ function updateIns(target, ...others) { if (others[others.length - 1] === target) return target; const keys = getInsKeys(target); keys.forEach((k) => _updateObj(target, others, k)); return target; } /** * 根据与target对比,挑出与target同key不同value的key所组成的object * @param target * @param objs 相当于assign(...objs) 同样的key只会取最后一个 * @param compareFn */ function pickUpdated(target, objs, compareFn = (a, b) => a === b || (isNaN(a) && isNaN(b))) { return objReduce(target, (result, _v, k) => { forEachRight(objs, function (item) { if (item && hasOwn(item, k)) { if (!compareFn(target[k], item[k])) { result[k] = item[k]; } return false; } }); return result; }, {}); } // TODO 需要去除掉前面object里的undefined /* type A = { a: undefined, b: number } type Pick2<T, K extends keyof T> = { [NonNullable<T[K]>]: NonNullable<T[K]>; }; type K = keyof A type V = A[K] type B = Pick2<A, keyof A>*/ /** * 创建一个object 代替es6的动态key object 与Object.fromEntries一样 * @example * const k1 = "a",k2 = "b" * createObj([[k1, 1], [k2, 2]]); // {a:1, b:2} * @param entries * @return {{}} */ function createObj(entries) { return entries.reduce((initValue, item) => { if (!isArray(item) || item.length < 1) throw new TypeError('createObj args type error'); const [key, value] = item; if (key !== void 0) { initValue[key] = value; } return initValue; }, {}); } /** * @alias createObj */ const ObjFromEntries = createObj; /** * Object.keys * @param obj */ function objKeys(obj) { // Object.keys es5可以使用 return reduceObj(obj, (init, _v, k) => { init.push(k); return init; }, []); } /** * Object.values * @param obj */ function objValues(obj) { return reduceObj(obj, (init, v) => { init.push(v); return init; }, []); } /** * Object.entries * @param obj */ function objEntries(obj) { return reduceObj(obj, (init, v, k) => { init.push([k, v]); return init; }, []); } /** * obj[a] => obj.a 从getObjValueByPath中分离出来 * @param path * @param [objName = ""] */ function translateObjPath(path, objName = '') { let result = path; // obj[a] => obj.a result = result.replace(new RegExp(`^${objName}`), ''); result = result.replace(/\[([^\]]+)]/g, '.$1'); result = result.replace(/^\.|\[]/g, ''); return result; } /** * 通过object路径获取值 * @example * getObjValueByPath({a: {b: {c: 123}}}, "a.b.c") // => 123 * @param obj * @param path * @param [objName = ""] */ function getObjValueByPath(obj, path, objName = '') { const p = translateObjPath(path, objName); return p.split('.').reduce((init, v) => { if (!isBroadlyObj(init)) return undefined; return init[v]; }, obj); } /** * 通过object路径设置值 如果路径中不存在则会自动创建对应的对象 * @example * @param obj * @param path * @param value * @param onExist 当要改动位置已经有值时的回调 * @param [objName = ""] */ function setObjValueByPath(obj, path, value, onExist = (_a, b) => b, objName = '') { const p = translateObjPath(path, objName); const split = p.split('.'); const end = split.length - 1; split.reduce(([init, currentPath], key, index) => { currentPath = currentPath + (currentPath ? '.' + key : key); if (index === end) { if (hasOwn(init, key)) { value = onExist(init[key], value, true, currentPath); } init[key] = value; return [init[key], currentPath]; } if (!isBroadlyObj(init[key])) { init[key] = hasOwn(init, key) ? onExist(init[key], {}, false, currentPath) : {}; } return [init[key], currentPath]; }, [obj, '']); return obj; } /** * 获取object的路径数组 * @example * getObjPathEntries({a: 1}) // => [["[a]", 1]] * getObjPathEntries({a: 1},"obj") // => [["obj[a]", 1]] * @param obj * @param [objName = ""] */ function getObjPathEntries(obj, objName = '') { return reduceObj(obj, (init, v, k) => { const key = `${objName}[${k}]`; if (isBroadlyObj(v)) { init.push(...getObjPathEntries(v, key)); } else { init.push([key, v]); } return init; }, []); } // 根据路径还原整个object function revertObjFromPath(pathArr) { function getKV(path) { const [k, value] = path.split('=').map((item) => decodeURIComponent(item)); let key = k; let innerKey = ''; const reg = /(?:\[([^[\]]*)])|(?:\.\[?([^[\]]*)]?)/g; if (reg.test(key)) { innerKey = RegExp.$1 || RegExp.$2; key = key.replace(reg, ''); } // key = key.replace(/\[[^\[\]]*]/g, ""); return { key, value, innerKey }; } return pathArr.reduce((result, path) => { const { key, value, innerKey } = getKV(path); const resultValue = result[key]; // no-fallthrough switch (typeOf(resultValue)) { case 'undefined': if (!innerKey) { result[key] = value; } else { const arr = []; arr[innerKey] = value; result[key] = arr; } break; case 'string': result[key] = [resultValue]; // eslint-disable-next-line no-fallthrough case 'array': if (!innerKey) { result[key].push(value); } else { result[key][innerKey] = value; } } return result; }, {}); } // ie9+ 支持,不需要实现 /* export function objCreate(proto: any) { const origin = {}; // @ts-ignore origin.__proto__ = proto; return origin; }*/ function objFilter(obj, predicate = (v) => v) { return objReduce(obj, (init, v, k) => { if (predicate(v, k)) { init[k] = v; } return init; }, {}); } /** * 判断对象是否包含某个属性。 * 因为直接object.hasOwnProperty(key)的话object可能会是null,所以另外封装一个函数使用。 * 可以用作类型守卫:见example。 * * @example * * const o = { a: 1 }; * let k = 'a'; * k = 'c'; * // 报错需要在tsconfig.json设置 * // "suppressImplicitAnyIndexErrors": false, * // "noImplicitAny": true, * o[k] = 2; // 此处没有类型守卫会报错 * if (hasOwn(o, k)) { * o[k] = 3; // 有类型守卫,安全 * } * * @param obj * @param key */ function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } function isNative(value) { const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; const reIsNative = RegExp(`^${Function.prototype.toString .call(Object.prototype.hasOwnProperty) .replace(reRegExpChar, '\\$&') .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\])/g, '$1.*?')}$`); return isBroadlyObj(value) && reIsNative.test(value); } // 获取数据类型 function typeOf(target) { const tp = typeof target; if (tp !== 'object') return tp; return Object.prototype.toString.call(target).slice(8, -1).toLowerCase(); } function isObject(target) { return typeOf(target) === 'object'; } function isObjectLike(value) { const type = typeof value; return value != null && (type === 'object' || type === 'function'); } const isBroadlyObj = isObjectLike; function isArray(target) { return typeOf(target) === 'array'; } // 类数组对象 jq的实现方式 function isArrayLike(target) { // 检测target的类型 const type = typeOf(target); // string也是ArrayLike,但"length" in target会报错 if (type === 'string') return true; if ([/*"string",*/ 'null', 'undefined', 'number', 'boolean'].indexOf(type) > -1) return false; // 如果target非null、undefined等,有length属性,则length等于target.length // 否则,length为false const length = !!target && 'length' in target && target.length; // 如果target是function类型 或者是window对象 则返回false if (type === 'function' || target === window) { return false; } // target本身是数组,则返回true // target不是数组,但有length属性且为0,例如{length : 0},则返回true // target不是数组,但有length属性且为整数数值,target[length - 1]存在,则返回true return (type === 'array' || length === 0 || (isNumber(length) && length > 0 && length - 1 in target)); } function isString(target) { return typeOf(target) === 'string'; } function isNumber(target) { return typeOf(target) === 'number'; } function isFunction(target) { return typeOf(target) === 'function'; } function isBoolean(target) { return typeOf(target) === 'boolean'; } function isUndefined(target) { return target === void 0; } // type t = "number" | "string" | "object" | "array" | "function" | "undefined" | "null" | "boolean" | "regexp" /** * 用typeIn("123", ["string", "number"]) 代替 typeOf("123") === "string" || typeOf("123") === "number" * @description 注意: 只能判断typeOf能够判断的类型 不能判断是否是NaN 是否是"" * @param target * @param types */ function inTypes(target, types) { const type = typeOf(target); if (!isArray(types)) throw TypeError('inTypes param types expected Array<string> but received ' + type); return types.indexOf(type) > -1; } // 参考is-promise function isPromiseLike(target) { const type = typeof target; return (!!target /*null也是object*/ && (type === 'object' || type === 'function') && typeof target.then === 'function'); } // 有native isNaN函数 但是 {} "abc"会是true function isNaN(target) { // return String(target) === "NaN"; // "NaN" 会被判断为true return isNumber(target) && target !== target; } // 判断是否是空object function isEmptyObject(target) { if (!isObject(target)) return false; for (const i in target) { return i === undefined; } return true; } // 判断是否是空值 undefined, null, "", [], {} ,NaN都为true function isEmpty(target) { // TO DO 可以替换array里的includes if (includes([undefined, null, '', NaN], target)) return true; // if (includes([undefined, null, "", NaN], target)) return true; switch (typeOf(target)) { case 'array': return !target.length; case 'object': // {a(){}} 使用JSON.stringify是会判断为空的 // return JSON.stringify(target) === "{}"; return isEmptyObject(target); } return false; } function isEqual(a, b) { if (a === b) return true; const aType = typeOf(a); const bType = typeOf(b); if (aType !== bType) return false; // noinspection FallThroughInSwitchStatementJS switch (aType) { case 'boolean': case 'string': case 'function': return false; case 'number': return isNaN(b); // 只有数组或者object不相等的时候才去对比是否相等 case 'array': case 'object': default: return objectIsEqual(a, b); } } function objectIsEqual(obj1, obj2) { if (obj1 === obj2) return true; for (const key in obj1) { const value1 = obj1[key]; const value2 = obj2[key]; if (!isEqual(value1, value2)) { return false; } } return true; } function isSameType(a, b) { return typeOf(a) === typeOf(b); } function isIterable(target) { try { // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _ of target) { break; } return true; } catch (e) { return false; } } function isPercent(value) { const reg = /^\d+(\.\d+)?%$/; return reg.test(value.trim()); } /** * 是否包含中文 * @param value */ function isIncludeChinese(value) { return /[\u4e00-\u9fa5]/.test(value); } /** * 是否正数 * @param value */ function isInteger(value) { return value % 1 === 0; } /** * @example * isArrayObj(Object.assign([1,2], {b: "1", c: "2"})) // => true * isArrayObj([]) // => false * @param value */ function isArrayObj(value) { const keys = objKeys(value); const reg = /\d+/; return isArray(value) && keys.some((i) => !reg.test(String(i))); } /** * @description len与end两个都有值时,以小的为准 * @example * // returns [0, 1] * createArray({end: 2}); * @example * // returns [0, 1] * createArray({start: 0, end: 2}); * @example * // [1, 1] * createArray({start:0, end:2, len:2, fill:1}); * @example * // returns [1, 2] * createArray(start: 0, len: 2, fill: item => item+1); */ function createArray({ start = 0, end, len, fill, }) { let e = start; if (len && end) { e = Math.min(start + len, end); } else { if (len !== undefined) { e = start + len; } if (end !== undefined) { e = end; } } let callback; switch (typeOf(fill)) { case 'function': callback = fill; break; case 'undefined': case 'null': callback = (i) => i; break; default: callback = () => fill; } const arr = []; for (let item = start, index = 0; item < e; item++, index++) { arr.push(callback(item, index)); } return arr; } // 注意:写成下面这种方式(把ArrayLike<T>提取到范型A): // function forEach<T, A extends ArrayLike<T>>( // arr: A, // callbackFn: (value: T, index: number, array: A) => any | false, // elseCB?: () => void, // ): boolean // 在使用时回调函数的value是不会有类型推导的,value类型将是 unknown // ie9支持 // forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; /** * @param arr * @param callbackFn * @param elseCB 类似于Python的for else中的else, * 只会在完整的遍历后执行,任何一个break都不会触发 * @returns {boolean} isDone */ function forEach(arr, callbackFn, elseCB) { // 不能直接把arr.length放进循环,否则在循环里新增的话length会变长,原生的不会变长 const len = arr.length || 0; // if (!isArrayLike(arr)) throw new TypeError(); for (let i = 0; i < len; i++) { if (callbackFn(arr[i], i, arr) === false) return false; } elseCB && elseCB(); return true; } /** * 跟promiseQueue类似,不过此函数是callback异步,重点在callback * @param arr * @param cbAsync 异步回调 */ async function forEachAsync(arr, cbAsync) { // 不能直接把arr.length放进循环,否则在循环里新增的话length会变长,原生的不会变长 const len = arr.length; // if (!isArrayLike(arr)) throw new TypeError(); for (let i = 0; i < len; i++) { const v = await cbAsync(arr[i], i, arr); if (v === false) break; } } async function mapAsync(arr, cbAsync) { const result = []; await forEachAsync(arr, async (v, k, a) => { const item = await cbAsync(v, k, a); result.push(item); }); return result; } async function reduceAsync(arr, callbackFn, initValue) { const len = arr.length; if (!len) { if (initValue === void 0) throw new Error('Reduce of empty array with no initial value'); return Promise.resolve(initValue); } let previousValue = initValue !== null && initValue !== void 0 ? initValue : (await arr[0]()); for (let i = initValue ? 0 : 1; i < len; i++) { const item = arr[i]; const curValue = await callbackFn(previousValue, item, i, arr); if (curValue === false) break; previousValue = curValue; } return previousValue; } function forEachRight(arr, callbackFn) { for (let i = arr.length - 1; i > -1; i--) { if (callbackFn(arr[i], i, arr) === false) break; } } // from<T, U>(iterable: Iterable<T> | ArrayLike<T>, mapfn: (v: T, k: number) => U, thisArg?: any): U[]; function from(iterable, mapFn = (value) => value) { const arr = []; if (isArrayLike(iterable)) { forEach(iterable, (v, k) => { arr.push(mapFn(v, k)); }); } else { for (const v of iterable) { arr.push(mapFn(v)); } } return arr; } // filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[] function filter(arr, callbackFn) { // if (!isArrayLike(arr)) throw new TypeError(); const fList = []; const len = arr.length; for (let i = 0; i < len; i++) { const value = arr[i]; if (callbackFn(value, i, arr)) { fList.push(value); } } return fList; } // includes(searchElement: T, fromIndex?: number): boolean function includes(arr, searchElement, fromIndex = 0) { // if (!isArrayLike(arr)) throw new TypeError(); const len = arr.length; for (let i = fromIndex; i < len; i++) { const item = arr[i]; if (typeof searchElement === 'function') { if (searchElement(item, i, arr)) return true; } else { if (item === searchElement) return true; } if (isNaN(item) && isNaN(searchElement)) return true; } return false; } // find<S extends T>(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined; // find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined; function find(arr, predicate) { // if (!isArrayLike(arr)) throw new TypeError(); // if (!isFunction(predicate)) return; // 在typescript中有类型检查,不需要这一句 const len = arr.length; for (let i = 0; i < len; i++) { const item = arr[i]; if (predicate(item, i, arr)) return item; } } function findIndex(arr, predicate) { // if (!isFunction(predicate)) return -1; // 在typescript中有类型检查,不需要这一句(用call和apply调用无法检查,还是加上) const len = arr.length; for (let i = 0; i < len; i++) { const item = arr[i]; if (predicate(item, i, arr)) return i; } return -1; } function findIndexRight(arr, predicate) { // if (!isFunction(predicate)) return -1; // 在typescript中有类型检查,不需要这一句(用call和apply调用无法检查,还是加上) const end = arr.length - 1; for (let i = end; i >= 0; i--) { const item = arr[i]; if (predicate(item, i, arr)) return i; } return -1; } function flat(target, depth = 1) { function innerFlat(innerArr, innerDepth = 0) { if (!isArray(innerArr) || innerDepth++ === depth) return innerArr; const result = []; for (let i = 0; i < innerArr.length; i++) { const newItem = innerFlat(innerArr[i], innerDepth); result.push(...(isArray(newItem) ? newItem : [newItem])); } return result; } return innerFlat(target); } /** * 二分查找item * @param arr 要查找的数组 * @param handler 判断条件 item => target - item 返回值为0时为要找的值,小于0则往前找,大于0往后找 * @param pcz 不用传 */ function binaryFind2(arr, handler, pcz = 0) { if (arr.length === 0) return undefined; let result; let middleIndex = Math.floor(arr.length / 2); const item = arr[middleIndex]; // 当前index = middleIndex + pcz const value = handler(item, middleIndex + pcz, arr); if (value === 0) { return item; } else { if (arr.length === 1) { return undefined; } // 如果target大于当前item值,则获取middle右边,否则相反 if (value > 0) { middleIndex++; result = binaryFind2(arr.slice(middleIndex), handler, pcz + middleIndex); } else { result = binaryFind2(arr.slice(0, middleIndex), handler, pcz); } } return result; } function binaryFind(arr, handler) { let start = 0; let end = arr.length; while (start < end) { const mid = ~~((end + start) / 2); const item = arr[mid]; const v = handler(item, mid, arr); if (v === 0) { return item; } else if (v > 0) { start = mid + 1; } else { end = mid; } } return undefined; } /** * 二分查找item index * @param arr 要查找的数组 * @param handler 判断条件 item => target - item 返回值为0时为要找的值,小于0则往前找,大于0往后找 */ function binaryFindIndex(arr, handler) { if (arr.length === 0) return -1; let start = 0; let end = arr.length; do { const middleIndex = Math.floor((end - start) / 2) + start; const value = handler(arr[middleIndex], middleIndex, start, end); if (value === 0) { return middleIndex; } else if (value > 0) { start = middleIndex + 1; } else { end = middleIndex; } } while (end > start); return -1; } /** * item插入到数组,在原数组中改变 * @param insert 插入的item * @param to 要插入的位置 如果to是函数的话没有找到则不会插进数组 * @param array 要插入item的数组 * @param after 默认插到前面去 * @param reverse 是否反向遍历 */ function insertToArray(insert, to, array, { after = false, reverse = false } = {}) { const inserts = castArray(insert); let index = to; if (isFunction(to)) { index = (reverse ? findIndexRight : findIndex)(array, (v, k, a) => { return to(v, k, a, insert); }); if (index === -1) { return -1; } } else { if (to < 0) { index = 0; } else if (to > array.length) { index = array.length - (after ? 1 : 0); } } after && index++; array.splice(index, 0, ...inserts); return index; } function arrayRemoveItem(item, array) { const index = array.indexOf(item); if (index === -1) return; return array.splice(index, 1)[0]; } function arrayRemoveItemsBy(by, array) { const removedItems = []; forEachRight(array, (v, k, a) => { if (!by(v, k, a)) return; const item = array.splice(k, 1)[0]; removedItems.unshift(item); }); return removedItems; } function unique(target, isRepeatFn) { if (!target.length) return target; const fn = isRepeatFn || ((v1, v2) => v1 === v2 || (isNaN(v1) && isNaN(v2))); const result = [target[0]]; for (let i = 1, len = target.length; i < len; i++) { const item = target[i]; if (result.some((resItem) => fn(resItem, item))) continue; result.push(item); } return result; } /** * @example * castArray([1]); // [1] * @example * castArray(1); // [1] * @param value */ function castArray(value) { return (isArray(value) ? value : [value]); } /** * 数组分片 * @example * chunk([0,1,2,3,4,5,6], 3) // => [[0,1,2],[3,4,5],[6]] * @param arr * @param chunkLen */ function chunk(arr, chunkLen) { if (!isArray(arr)) throw new TypeError('chunk param arr type error'); if (chunkLen < 1) return arr.slice(); const result = []; let i = 0; while (i < arr.length) { result.push(arr.slice(i, (i += chunkLen))); } return result; } /** * 判断min <= num <= max * @param value * @param [min = Number.MIN_SAFE_INTEGER] * @param [max = Number.MAX_SAFE_INTEGER] */ function inRange$1(value, [min = -Infinity, max = Infinity]) { return min <= value && value <= max; } /** * @param value {number} * @param ranges {[number,number][]} */ function inRanges(value, ...ranges) { return ranges.some((item) => inRange$1(value, item)); } function groupBy(arr, key, defaultKey = '*') { const cb = isFunction(key) ? key : (item) => item[key]; return arr.reduce((result, item) => { var _a; const k = (_a = cb(item, result)) !== null && _a !== void 0 ? _a : defaultKey; if (!hasOwn(result, k)) { result[k] = [item]; } else { result[k].push(item); } return result; }, {}); } /** * 查找是否items中任何一个在list中 * @template T * @param {T[]} items * @param {T[]} list * @param {(item: T) => boolean} cb * @return {boolean} */ function someInList(items, list, cb = (v, _, arr) => includes(arr, v)) { return items.some((item, index) => { return cb(item, index, list); }); } /** * Number.prototype.toLocaleString 也能转成千位分隔数字字符串 * 千位分隔 1,234,567,890 * @param num * @param [isFormatDecimalPlaces=false] 是否格式化小数位 * @param [delimiter = ","] */ function thousandFormat(num, isFormatDecimalPlaces = false, delimiter = ',') { // 123123.1111 => 123,123.1,111 // return String(num).replace(/\B(?=(?:\d{3})+(?!\d))/g, delimiter); const split = String(num).split('.'); split[0] = split[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, delimiter); if (split.length === 1 || !isFormatDecimalPlaces) return split.join('.'); split[1] = split[1].replace(/(\d{3})/g, `$1${delimiter}`); return split.join('.').replace(new RegExp(`${delimiter}$`), ''); } function strTemplate(str, ...params) { /* // es5; typescript不需要str, ...params参数` var args = Array.prototype.slice.call(arguments, 0); if (!args.length) return ""; var str = args[0]; var params = args.slice(1); */ return str.replace(/%s/g, function () { return params.length ? params.shift() : ''; }); } /** * 给长度不满足要求的字符串添加前缀 strFillPrefix * @param target * @param maxLen * @param [fill=' '] 默认fill=" " * @param [clearMore=false] */ function strPadStart(target, maxLen, fill = ' ', clearMore = false) { if (target.length >= maxLen || fill === '') { if (clearMore) { // 切掉多余的部分 return target.substr(target.length - maxLen); } return target; } // 缺少的长度 const lessLen = maxLen - target.length; // 填充补足不够的部分 while (fill.length < lessLen) { fill += fill; } // 切除多余部分 fill是多个字符时补足的长度可能会超出 fill = fill.substr(0, lessLen); // 补足前缀 return fill + target; } /** * 给长度不满足要求的字符串添加后缀 strFillPrefix * @param target * @param maxLen * @param [fill=" "] 默认fill=" " * @param [clearMore=false] */ function strPadEnd(target, maxLen, fill = ' ', clearMore = false) { if (target.length >= maxLen || fill === '') { if (clearMore) { return target.substr(0, maxLen); } return target; } const lessLen = maxLen - target.length; const end = strPadStart(target, maxLen, fill).substr(0, lessLen); return target + end; } const numberArr = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']; const sbq = ['十', '百', '千']; const units = ['', ...sbq, '万', ...sbq, '亿']; const unitLen = units.length; /** * 阿拉伯数字转为中文数字 * @param number */ const number2Chinese = function (number) { let key = ~~number; let chineseNumber = ''; let times = 0; // 个位数 if (number >= 0 && number < 10) return number2Chinese.numbers[number]; while (key >= 1 && times < unitLen) { const unit = number2Chinese.units[times]; // 11 % 10 => 一 const end = number2Chinese.numbers[key % 10]; // 101 0没有单位 if (end !== number2Chinese.numbers[0]) { chineseNumber = unit + chineseNumber; } // 11 => 一十一 => 十一 if (!(key === 1 && times === 1)) { chineseNumber = end + chineseNumber; } key = ~~(key / 10); times++; } // 一万零零一 => 一万零一 | 一万零零零 => 一万 return chineseNumber.replace(/(零+$)|((零)\3+)/g, '$3'); }; number2Chinese.units = [...units]; number2Chinese.numbers = [...numberArr]; /** * 中文转为阿拉伯数字 * @param chineseNumber */ const chinese2Number = function (chineseNumber) { if (new RegExp(`([^${chinese2Number.units.join() + chinese2Number.numbers.join()}])`).test(chineseNumber)) { throw new TypeError('发现不符合规则的字符(必须在units和numbers里存在的字符):' + RegExp.$1); } // 用万和亿分割 const arr = chineseNumber.split(new RegExp(`[${chinese2Number.units[4]}${chinese2Number.units[8]}]`, 'g')); const numberArr = arr.map((it) => { let res = 0; let unit = 1; // 从个位数往大数累加 for (let i = it.length - 1; i > -1; i--) { const item = it[i]; const number = chinese2Number.numbers.indexOf(item); if (number > 0) { res += number * unit; } const unitIndex = chinese2Number.units.indexOf(item); unit = unitIndex > 0 ? 10 ** unitIndex : unit; } // 以十开头的要单独列出来 例如十一完全体是一十一 if (it[0] === chinese2Number.units[1]) { res += 10; } return res; }); // 把分割开的数字拼接回去 return numberArr.reverse().reduce((res, item, index) => { return res + 10000 ** index * item; }, 0); }; chinese2Number.units = [...units]; chinese2Number.numbers = [...numberArr]; /** * 从字符串中删除指定字符串(from)中重复的第n(num)个字符串(str) * @example * removeSlashByNum("123/456/78", 2, "\/"); // "123/45678" * @param from * @param num * @param removeStr */ function removeStrByNum(from, num, removeStr) { let times = 1; return String(from).replace(new RegExp(removeStr, 'g'), (v) => (times++ === num ? '' : v)); } /** * 切割字符串 * @param str * @param start * @param {number?} [end=str.length] end为正数时与String.prototype.substring效果一致,为负数时相当于end+str.length */ function subString(str, start, end = str.length) { if (end < 0) { end += str.length; } return str.substring(start, end); } /** * 与String.prototype.repeat相同 * String.prototype.repeat支持到ie11 * @param value * @param repeatCount */ function strRepeat(value, repeatCount) { if (repeatCount < 0 || repeatCount * value.length > strRepeat.MAX_STR_LENGTH) throw new RangeError('strRepeat Invalid repeatCount value'); let result = ''; if (value === '') return ''; while (repeatCount-- > 0) { result += value; } return result; } strRepeat.MAX_STR_LENGTH = 512 * 1024 * 1024; /** * 根据模板创建出字符串 除了面试题找不到应用场景的函数 * @example * smartRepeat("2[2[a]2[b]]") // returns "aabbaabb" * @param format */ function smartRepeat(format) { let exec; const re = /(\d+)\[([^[\]]+)](?!\d+\[)/; while ((exec = re.exec(format))) { const [, count, repeatValue] = exec; // 第一种方式 format = format.replace(re, strRepeat(repeatValue, Number(count))); // 第二种方式 // const start = format.substring(0, exec.index); // const end = format.substring(exec.index + exec[0].length); // format = start + strRepeat(repeatValue, count) + end; } return format; } /** * 首字母大写 * @param value */ function capitalize(value) { if (!value.length) return value; const first = value[0]; return `${first.toUpperCase()}${value.substring(1).toLowerCase()}`; } /** * 从驼峰转其他命名格式 * @param {string} value * @param [delimiter='_'] * @param [toUpperCase=false] // 为true时 转为全大写的格式 * @return {string} */ function fromCamel(value, delimiter = '_', toUpperCase = false) { const res = value.replace(/([A-Z]+)/g, (_p1, p2, index) => { return (index > 0 ? delimiter : '') + p2.toLowerCase(); }); return toUpperCase ? res.toUpperCase() : res; } /** * 其他转驼峰 * @param {string} value * @param {string | RegExp} delimiter * @param {boolean} toUpperCamelCase * @return {string} */ function toCamel(value, delimiter = '_', toUpperCamelCase = false) { if (!value.length) return value; const reg = typeof delimiter === 'string' ? new RegExp(delimiter + '+') : delimiter; const join = value.split(reg).map((i) => capitalize(i)); if (!toUpperCamelCase && join.length) { join[0] = join[0].toLowerCase(); } return join.join(''); } /** * 获取字符串实际长度,String.prototype.length获取的是utf-16的长度 * * @example * * const str = '😂👱👬👨‍👩‍👧👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👧👩‍👧‍👧; // 8个表情,包含组合表情 * console.log(str.length); // 49 * console.log(getStringLen(str)); // 8 * * const str2 = '🙎🏿'; // 黑皮肤表情 * console.log(str2.length); // 4 * console.log(getStringLen(str2)); // 1 * * @param value 要获取长度的字符串 * @returns 字符串实际长度 */ function getStringLen(value) { // https://www.zhihu.com/question/38324041 // underscore.js toArray // const reg = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; // 参考 https://juejin.cn/post/6941276804472635405 // '😂1👱1👬1👨‍👩‍👧1👨‍👩‍👧‍👦23'.split('') // 8205 是通过 [...'👨‍👩‍👧‍👦123123123'].forEach((v) => console.log(v.codePointAt(0))); 获取的 // const emojiDelimiter = String.fromCodePoint(8205); // emoji分隔符:该字符看上去很像空格 const emojiDelimiter = '\u200D'; // (8205).toString(16) const symbol = '[\ud800-\udbff][\udc00-\udfff]'; // 👱🏼‍👱🏿‍皮肤颜色 参考 https://zhuanlan.zhihu.com/p/328516890 const skinColor = '\ud83c[\udffc-\udfff]'; return (value // 先匹配组合表情符号 .replace(new RegExp(`${symbol}(${emojiDelimiter}${symbol})+`, 'g'), '_') // 再匹配有皮肤的表情符号 .replace(new RegExp(`${symbol}${skinColor}`, 'g'), '_') // 然后匹配普通表情符号 .replace(new RegExp(symbol, 'g'), '_').length); } /** * @param millisecond * @param {string} [format=d天hh时mm分ss秒] - 格式化模板 */ function number2Date(millisecond, format = 'd天hh时mm分ss秒') { let result = format; const seconds = millisecond / 1000; const obj = { 's+': seconds % 60, 'm+': ~~(seconds / 60) % 60, 'h+': ~~(seconds / (60 * 60)) % 24, // 'd+': ~~(seconds / (60 * 60 * 24)) }; // 有多少天就显示多少天,但不会补0 const days = ~~(seconds / (60 * 60 * 24)); result = result.replace(/d+/, String(days)); for (const k in obj) { const reg = new RegExp('(' + k + ')'); if (reg.test(result)) { const s1 = RegExp.$1; const v = obj[k]; let value = String(v).padStart(s1.length, '0'); value = value.substring(value.length - s1.length); result = result.replace(s1, value); } } return result; } /** * 比较两个日期相差年天时分秒 用于倒计时等 * @param start * @param end * @param [format="y年d天hh时mm分ss秒"] */ function dateDiff(start, end, format = 'y年d天hh时mm分ss秒') { let result = format; if (start.getTime() > end.getTime()) { [start, end] = [end, start]; } const targetTime = end.getTime() - start.getTime(); const seconds = ~~(targetTime / 1000); const obj = { 'S+': targetTime % 1000, 's+': seconds % 60, 'm+': ~~(seconds / 60) % 60, 'h+': ~~(seconds / (60 * 60)) % 24, 'd+': (function () { const day = ~~(seconds / (60 * 60 * 24)); // 如果要显示年,则把天余年,否则全部显示天 // 默认一年等于365天 return /y+/.test(result) ? day % 365 : day; })(), // "M+": 0, 'y+': ~~(seconds / (60 * 60 * 24 * 365)), }; for (const k in obj) { const reg = new RegExp('(' + k + ')'); if (reg.test(result)) { // 奇怪的bug 本地调试的时候RegExp.$1不准确,"s+"的时候$1是空字符串; 非调试的时候又没问题 const s1 = RegExp.$1; const v = obj[k]; let value = strPadStart(String(v), s1.length, '0'); // substring(start,end) start小于0的时候为0 substr(from,len)from小于0的时候为字符串的长度+from value = value.substring(value.length - s1.length); //手动切割00:00 m:s "00".length - "s".length,因为strPadStart当字符串长度大于length的话不会切割 result = result.replace(s1, value); } } return result; } /** * 格式化日期 * @param [formular="yyyy-MM-dd hh:mm:ss"] * @param date {Date} * @param seasonText {string[]} * @param weekText {string[]} * @returns String */ const formatDate = function (date, formular = 'yyyy-MM-dd hh:mm:ss', { seasonText = formatDate.seasonText, weekText = formatDate.weekText } = {}) { var _a; const o = { 'M+': () => date.getMonth() + 1, 'd+': () => date.getDate(), 'h+': () => date.getHours(), 'm+': () => date.getMinutes(), 's+': () => date.getSeconds(), q: () => { //季度 // 按月份区分的季度并不准确 const q = Math.floor((date.getMonth() + 3) / 3) - 1; return seasonText[q]; }, 'S+': () => date.getMilliseconds(), w: () => weekText[date.getDay()], //周 }; if (/(y+)/.test(formular)) { formular = formular.replace(RegExp.$1, strPadStart(String(date.getFullYear()), RegExp.$1.length, '0', true)); } for (const k in o) { if (new RegExp('(' + k + ')').test(formular)) { const s1 = RegExp.$1; const v = String((_a = o[k]) === null || _a === void 0 ? void 0 : _a.call(o)); // const value = s1.length === 1 ? v : ("00" + v).substr(String(v).length); formular = formular.replace(s1, strPadStart(v, s1.length, '0')); } } return formular; }; formatDate.weekText = ['日', '一', '二', '三', '四', '五', '六']; formatDate.seasonText = ['春', '夏', '秋', '冬']; /** * 字符串转为date对象 因为苹果手机无法直接new Date("2018-08-01 10:20:10")获取date * @param date 格式:yyyy-MM-dd hh:mm:ss * @returns {Date} */ function getDateFromStr(date) { // 检测非数字、非/、非:、非- if (!date || /[^/\d: -]/.test(date)) return null; // 去除不符合规范的字符串 const arr = date.split(/[- :/]/).map((item) => Number(item)); if (arr.length < 6) { for (let i = arr.length; i < 6; i++) { arr[i] = i < 3 ? 1 : 0; // 年月日最小为1 } } return new Date(arr[0], arr[1] - 1, arr[2], arr[3], arr[4], arr[5]); } const str2Date = getDateFromStr; function sleep(ms) { return new Promise((res) => setTimeout(res, ms)); } function createTimeCountUp() { const startTime = Date.now(); return function () { return Date.now() - startTime; }; } /* /!** * 创建一个倒计时函数 * @param countDown 目标毫秒 *!/ export function createTimeCountDown(countDown: number): () => number { const startTime = Date.now(); return function () { const ms = Date.now() - startTime; return countDown - ms; }; }*/ /** * 创建一个倒计时函数 * @param countDown 目标毫秒 */ function createTimeCountDown(countDown) { const timeCountUp = createTimeCountUp(); return function () { return countDown - timeCountUp(); }; } /** * 获取某月最后一天的date * @param month */ function getTheLastDateOfAMonth(month) { const lastDate = new Date(month.getTime()); lastDate.setMonth(month.getMonth() + 1); lastDate.setDate(0); return lastDate; } /** * 获取指定某年月份(month)第n(nth)个星期几(weekday)的Date * @param month * @param nth nth为负的时候从月末开始倒数 * @param [weekday=0] 0和7都是周日 */ function getMonthTheNthWeekday(month, nth, weekday = 0) { // if (!nth || weekday < 0 || weekday > 7) return null; if (!nth || !inRange$1(weekday, [0, 7])) return null; const monthTime = month.getTime(); const endDate = getTheLastDateOfAMonth(month); let date; if (nth > 0) { date = new Date(monthTime); date.setDate(1); } else { date = new Date(endDate.getTime()); } weekday = weekday === 0 ? 7 : weekday; const diff = weekday - date.getDay(); if (nth > 0) { diff >= 0 && nth--; } else { diff <= 0 && nth++; } const dayDate = nth * 7 + date.getDate() + diff; if (dayDate > endDate.getDate() || dayDate < 1) { return null; } date.setDate(dayDate); return date; } /** * 获取毫秒数 * @param [days=0] * @param [hours=0] * @param [minutes=0] * @param [seconds=0] */ function getMilliseconds({ days = 0, hours = 0, minutes = 0, seconds = 0, } = {}) { const sec