剖析moyan-utils前端工具库,它是针对复杂业务场景使用的JS类库

2,085 阅读7分钟

文章前言

永远把别人对你的批评记在心里,别人的表扬,就把它忘了。Hello 大家好~!我是南宫墨言QAQ

本文主要是剖析moyan-utils这个前端工具库,该工具库是本人在一次突发奇想下,想着记录下平时在项目中遇到复杂业务场景下使用到的工具函数提炼到这个工具库,通过不断的优化改进,供后续项目中遇到相同业务场景中能直接服用。

本人欢迎有想法,热衷技术的小伙伴一起共建这个前端工具库,你可以通过在本文下方留言、GitHub上提issues或pr来参与共建项目,项目地址

针对文章中的功能函数有写的不好的地方,欢迎大伙在评论区指出讨论,谢谢

观看到文章最后的话,如果觉得不错,可以点个关注或者点个赞哦!感谢~❤️

文章主体

感谢各位观者的耐心观看,moyan-utils前端工具库的剖析正片即将开始,且听南宫墨言QAQ娓娓道来

image.png

常用的判断函数

函数名称函数作用函数作者函数更新时间
isEmpty判断数据是否是空对象/空数组/空字符串南宫墨言QAQ2023-07-17
isMatchDataType判断数据是否匹配相应的数据类型南宫墨言QAQ2023-07-17

数据结构相关

函数名称函数作用函数作者函数更新时间
chunk将数组拆分成多个 size 长度的区块,并将这些区块组成一个新数组南宫墨言QAQ2023-07-17
cloneDeep深拷贝南宫墨言QAQ2023-07-17
flatten数组扁平化南宫墨言QAQ2023-07-17
formatNumber数字千分位展示并显示n位小数南宫墨言QAQ2023-07-17
get根据 object对象的path路径获取值南宫墨言QAQ2023-07-17
getDataType获取JavaScript数据类型南宫墨言QAQ2023-07-17
omit忽略对象选中的属性南宫墨言QAQ2023-07-17
omitBy生成对象中经函数为假值的属性南宫墨言QAQ2023-07-17
pick生成对象选中的属性南宫墨言QAQ2023-07-17
pickBy生成对象中经函数为真值的属性南宫墨言QAQ2023-07-17

高级函数

函数名称函数作用函数作者函数更新时间
memoize记忆函数南宫墨言QAQ2023-07-17

数学计算

函数名称函数作用函数作者函数更新时间
getRandomColor获取随机颜色南宫墨言QAQ2023-07-17

常用的判断函数

isEmpty

用于判断数据是否为空,包括空对象、空数组、空字符串,返回值是布尔值

代码实现:

/**
 * 判断数据是否是空对象/空数组/空字符串
 * @param {String | Array<T> | object} source 需要验证的对象
 * @returns {Boolean} 返回布尔值
 */
 export function isEmpty<T>(source: String | Array<T> | object) {
  if(source.constructor === Object) return Reflect.ownKeys(source).length === 0 
  return (source as  String | Array<T>).length === 0
}

使用例子:

import { isEmpty } from "moyan-utils"

isEmpty({}) => true
isEmpty({ a: 1 }) => false

isEmpty([]) => true
isEmpty([1, 2, 3]) => false

isEmpty('') => true
isEmpty('123') => false

isMatchDataType

用于判断数据是否匹配相应的数据类型,返回值是布尔值

代码实现:

import { _DataType } from '../types';
import { getDataType } from '../getDataType';

/**
 * 判断数据是否匹配相应的数据类型
 * @param {_DataType} type 数据类型
 * @param {T} source 检测的数据源
 * @returns {boolean} 返回布尔值
 * @example
 * const array = [1, 2]
 * isMatchDataType(JSDataType.Array, array) => true
 */
export function isMatchDataType<T>(type: _DataType, source: T) {
  return getDataType(source) === type;
}

使用例子:

import { isMatchDataType } from "moyan-utils"

const array = [1, 2]
isMatchDataType(JSDataType.Array, array) => true

数据结构相关

chunk

将数组拆分成多个size长度的区块,返回值是这些区块组成的新数组

代码实现:

/**
 * 将数组拆分成多个 size 长度的区块,并将这些区块组成一个新数组
 * @param {T[]} source 需要处理的数组
 * @param {number} size 每个数组区块的长度
 * @returns {T[][]} 返回一个包含拆分区块的新数组(注:相当于一个二维数组)
 */
export function chunk<T>(source: T[], size = 1) {
  /** size小于1时拆分没有意义,直接返回空数组 */
  if (size < 1) return [];
  const result = [];
  /** 遍历数组,每次递增size */
  for (let i = 0, len = source.length; i < len; i += size) {
    /** 截取对应长度的数组并塞到result中 */
    result.push(source.slice(i, i + size));
  }
  return result;
}

使用例子:

import { chunk } from "moyan-utils"

const data = [1, 2, 3, 4, 5]
chunk(data, 1) => [ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ] ]
chunk(data, 3)) => [ [ 1, 2, 3 ], [ 4, 5 ] ]
chunk(data, 5) => [ [ 1, 2, 3, 4, 5 ] ]

cloneDeep

用于深拷贝JavaScript数据,返回值是拷贝后的数据

代码实现:

import { DataType } from '../types';
import { isMatchDataType } from '../isMatchDataType';

/**
 * 深拷贝
 * @param {T} obj 要拷贝的数据源
 * @param {WeakMap} cache 缓存的值
 * @returns {T} 拷贝后的数据
 */
export function cloneDeep<T extends unknown>(
  source: T,
  cache = new WeakMap()
): T {
  /** 如果不是对象或者是null,直接返回 */
  if (typeof source !== 'object' || source == null) {
    return source;
  }

  /** 如果已经缓存过,直接返回缓存的值 */
  if (cache.has(source as object)) {
    return cache.get(source as object);
  }

  /** 初始化返回结果 */
  let result: T, param!: T;

  /** 如果是日期对象,直接返回一个新的日期对象 */
  if (
    isMatchDataType(DataType.Date, source) ||
    isMatchDataType(DataType.RegExp, source)
  ) {
    param = source;
  }

  result = new (source as any).constructor(param);

  /** 如果是数组或对象,需要遍历 */
  if (
    isMatchDataType(DataType.Array, source as object) ||
    isMatchDataType(DataType.Object, source as object)
  ) {
    for (let key in source) {
      if ((source as object).hasOwnProperty(key)) {
        result[key] = cloneDeep(source[key], cache);
      }
    }
  }

  /** 如果是Set */
  if (isMatchDataType(DataType.Set, source)) {
    for (let value of source as unknown as Set<T>) {
      (result as Set<T>).add(cloneDeep(value, cache));
    }
  }

  /** 如果是Map */
  if (isMatchDataType(DataType.Map, source)) {
    for (let [key, value] of source as unknown as Map<T, T>) {
      (result as Map<T, T>).set(cloneDeep(key, cache), cloneDeep(value, cache));
    }
  }

  /** 缓存值 */
  cache.set(source as object, result);

  return result;
}

使用例子:

import { cloneDeep } from "moyan-utils"

const obj1 = { a: 1, b: 2, c: { d: 3, e: 4 }, f: 5, g: { h: 6 } }
const obj2 = cloneDeep(obj1) => { a: 1, b: 2, c: { d: 3, e: 4 }, f: 5, g: { h: 6 } }
obj1.c === obj2.c => true

const array1 = [{ id: 1, children: [{ id: 2 }, { id: 3 }] }, { id: 4 }]
const array2 = cloneDeep(array1) => [{ id: 1, children: [{ id: 2 }, { id: 3 }] }, { id: 4 }]
array2[2] === array1[2] => true

const array = [1, 2, 3, 4]
const set1 = new Set(array) => Set(4) { 1, 2, 3, 4 }
const set2 = cloneDeep(set1) => Set(4) { 1, 2, 3, 4 }

const json = '{"user1":"John","user2":"Kate","user3":"Peter"}'
const map1 = new Map(Object.entries(JSON.parse(json))) => Map(3) { 'user1' => 'John', 'user2' => 'Kate', 'user3' => 'Peter' }
const map2 = cloneDeep(map1) Map(3) => { 'user1' => 'John', 'user2' => 'Kate', 'user3' => 'Peter' }

const reg1 = new RegExp('abc', 'g') => /abc/g
const reg2 = cloneDeep(reg1) => /abc/g

const date1 = new Date() => 2023-04-21T15:17:57.128Z
const date2 = cloneDeep(date1) => 2023-04-21T15:17:57.128Z

flatten

用于将数组扁平化,返回值是是一个新的数组

代码实现:

import { DataType } from "../types";
import { isMatchDataType } from "../isMatchDataType";

/**
 * 数组扁平化
 * @param {T[]} source  要扁平化的数组
 * @param {Function} generateValue 处理函数获取对应的值
 * @returns {T[]} 返回扁平化后的数组
 */
export function flatten<T>(
  data: T[],
  generateValue?: (item: T) => T[] | undefined
): T[] {
  return data.reduce((acc, cur) => {
    let tmp: T | T[] = cur;
    const propertyValue = generateValue?.(cur);
    isMatchDataType(DataType.Object, cur) && propertyValue && (tmp = propertyValue);

    if (Array.isArray(tmp)) {
      const prev = propertyValue ? acc.concat(cur) : acc;
      return [...prev, ...flatten(tmp, generateValue)];
    } else {
      return [...acc, cur];
    }
  }, [] as T[]);
}

使用例子:

import { flatten } from "moyan-utils"

const numbers = [1, [2, [3, 4], 5], 6, [7, 8]];
flatten(numbers) => [1, 2, 3, 4, 5, 6, 7, 8]

const data = [
    {
        id: 1,
        name: 'item1',
        children: [
            {
                id: 2,
                name: 'item2',
                children: [
                    { id: 3, name: 'item3' },
                    { id: 4, name: 'item5' },
                ],
            },
            { id: 5, name: 'item5' },
        ],
    },
    { id: 6, name: 'item6' },
];

flatten(data, item => item.children) =>
 [
    { id: 1, name: 'item1', children: [ [Object], [Object] ] },
    { id: 2, name: 'item2', children: [ [Object], [Object] ] },
    { id: 3, name: 'item3' },
    { id: 4, name: 'item5' },
    { id: 5, name: 'item5' },
    { id: 6, name: 'item6' }
]

formatNumber

用于将数字千分位处理,并保留n位小数,返回值是处理后的值

代码实现:

/**
 * 数字千分位展示并显示n位小数
 * @param {number | string} source 需要格式化的值
 * @param {number} precision 保留几位小数
 * @returns {string | 0} 返回格式化的值或0
 */
export function formatNumber(
  source: number | string,
  precision?: number
): number | string {
  /** 判断是否为数字 */
  if (!isNaN(parseFloat(source as string)) && isFinite(source as number)) {
    source = Number(source);
    /** 处理小数点位数 */
    source = (typeof precision !== 'undefined'
      ? source.toFixed(precision)
      : source
    ).toString();

    /** 分离数字的整数和小数部分 */
    const parts = source.split('.');
    /** 对整数部分进行处理 */
    parts[0] = parts[0].replace(/(?=\B(\d{3})+$)/g, ',');
    return parts.join('.');
  }
  return 0;
}

使用例子:

import { formatNumber } from "moyan-utils"

const value = 6594375.676
formatNumber(6594375.67) => 6,594,375.676

formatNumber(6594375.676, 2) => 6,594,375.68

const str= '5974352.458'
formatNumber('5974352.458') =>5,974,352.458

formatNumber('5974352.458', 2) => 5,974,352.46

formatNumber('.') => 0

get

用于获取object对象的path路径对应的值,返回值是解析出来的value,如果 value 是 undefined 会以 defaultValue 取代

代码实现:

/**
 * 根据 object对象的path路径获取值。 如果解析 value 是 undefined 会以 defaultValue 取代
 * @param {object} source 要查询的对象
 * @param {Array|string} path 获取属性的路径
 * @param {*} [defaultValue] 为“未定义”解析值返回的值
 * @returns {*} 返回解析值
 */

export function get(
  source: { [key: string]: any },
  path: string | string[],
  defaultValue?: any
) {
  let result = source;
  /** 将path统一处理成字符串数组的形式 */
  const paths =
    typeof path === 'string' ? path.match(/[^\[\].]+/g) ?? [] : path;
  /** 遍历path,依次取出source中的属性值 */
  for (const key of paths) {
    result = result[key];
    /** 如果属性值不存在,直接返回defaultValue */
    if (result === undefined) return defaultValue;
  }
  return result;
}

使用例子:

import { get } from "moyan-utils"

const object = { 'a': [{ 'b': { 'c': 3 } }] }
get(object, 'a[0].b.c') => 3

get(object, ['a', '0', 'b', 'c']) => 3

get(object, 'a.b.c', 'default') => 'default'

getDataType

用于获取JavaScript数据类型。将数据源作为参数带入函数,返回值是该数据的具体数据类型

代码实现:

import { _DataType } from '../types'

/**
 * 获取JavaScript数据类型
 * @param {T} source 要检测的数据源
 * @returns {"Undefined" | "Object" | "Array" | "Date" | "RegExp" | "Function" | "String" | "Number" | "Boolean" | "Null" | "Symbol" | "Set" | "Map"} 返回数据类型
 */
export function getDataType<T>(source: T) {
  if (typeof source === 'undefined') return 'Undefined';
  return Object.prototype.toString.call(source).slice(8, -1) as _DataType
}

使用例子:

import { getDataType } from "moyan-utils"

getDataType(15) =>  Number

getDataType([1, 2, 3]) => Array

getDataType(null) => Null

产生背景:

众所周知,JavaScript中可以使用typeofinstanceof来判断数据类型,typeof会返回一个变量的基本类型,instanceof会返回一个布尔值,但这两种方法各有弊端。

使用typeof判断数据类型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

可以发现typeof可以判断基础数据类型(null除外),但是引用数据类型中,除了function类型以外,其他的也无法判断,故在判断数据类型方面舍弃此方案。

注:typeof null会输出object,是因为这是JavaScript存在的一个悠久Bug。在JavaScript的最初版本使用的是32位系统,为了性能考虑使用低位存储变量的类型信息,000开头代表的是对象,而null刚好表示为全0,所以JavaScript将它错误的判断为object

使用instanceof判断数据类型

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

1 instanceof Number // false
'1' instanceof String // false
undefined instanceof Object // false
true instanceof Boolean // false
null instanceof Object // false
[] instanceof Array // true
{} instacneof Object // true
console instanceof Object // true
console.log instanceof Function // true

可以发现instanceof能准确地判断复杂引用数据类型,但不能正确判断基础数据类型,所以该方案也被舍弃。

那么,我们可以采用Object.prototype.toString来判断数据类型,因为调用该方法,统一返回“[object Xxx]”的字符串,getDataType正是采用这种方法来获取数据类型的

omit

用于排除对象中选中的属性,返回值是排除后的新对象

代码实现:

/**
 * 忽略对象选中的属性
 * @param {object} object 来源对象
 * @param {string[] } paths 要被忽略的属性数组
 * @returns {object} 返回新对象
 */
export function omit<T extends object, K extends keyof T>(object: T, paths: K[]) {
  /** 获取对象的属性数组,然后筛出给定的key */
  return (Object.keys(object) as K[]).reduce((acc, key) => {
    if (!paths.includes(key)) {
      (object).hasOwnProperty(key) && (acc[key] = object[key]);
    }
    return acc;
  }, {} as Pick<T, K>);
}

使用例子:

import { omit } from "moyan-utils"

const object = { a: 1, b: '2', c: 3 }
omit(object, ['a', 'c']) => { b: '2' }

omitBy

用于生成对象经函数为假值的属性,返回值是生成的新对象

代码实现:

/**
 * 生成经 predicate 判断为假值的属性的对象
 * @param {object} object 来源对象
 * @param {Function} predicate 调用每一个属性的函数
 * @returns {object} 返回新对象
 */
export function omitBy<T>(
  object: T,
  predicate: (item: T[Extract<keyof T, string>], key: keyof T) => {}
) {
  const result = {} as { [K in keyof T]: T[K] };
  for (const key in object) {
    const curProperty = object[key];

    if (!predicate(curProperty, key)) {
      result[key] = curProperty;
    }
  }

  return result;
}

使用例子:

import { omitBy } from "moyan-utils"

const object = { a: 1, b: '2', c: 3 };
omitBy(object, (item) => typeof item === 'number') =>  { b: '2' }

pick

用于生成对象中选中的属性,返回值是选中后的新对象

代码实现:

/**
 * 生成选中属性的对象
 * @param {object} object 来源对象
 * @param {string[] } paths 要被选中的属性数组
 * @returns {object} 返回新对象
 */
export function pick<T extends object, K extends keyof T>(object: T, paths: K[] = []) {
  /** 筛出给定的key */
  return paths.reduce((acc, key) => {
    object.hasOwnProperty(key) && (acc[key] = object[key]);
    return acc;
  }, {} as Pick<T, K>);
}

使用例子:

import { pick } from "moyan-utils"

const object = { a: 1, b: '2', c: 3 }
pick(object, ['a', 'c']) => { a: 1, c: 3 }

pickBy

用于生成对象经函数为真值的属性,返回值是生成的新对象

代码实现:

/**
 *  生成经 predicate 判断为真值的属性的对象
 * @param {object} object 来源对象
 * @param {Function} predicate 调用每一个属性的函数
 * @returns {object} 返回新对象
 */
export function pickBy<T>(
  object: T,
  predicate: (item: T[Extract<keyof T, string>], key: keyof T) => {}
) {
  const result = {} as { [K in keyof T]: T[K] };

  for (const key in object) {
    const curProperty = object[key];

    if (predicate(curProperty, key)) {
      result[key] = curProperty;
    }
  }

  return result;
}

使用例子:

import { pickBy } from "moyan-utils"

const object = { a: 1, b: '2', c: 3 }
pickBy(object, (item) => typeof item === 'number') =>  { 'a': 1, 'c': 3 }

高级函数

memoized

用于生成缓存化的函数,便于多次使用相同的操作而直接返回缓存的的值,返回值是一个函数

代码实现:

/**
 * 记忆函数
 * @param {Function} fn 需要缓存化的函数
 * @param {Function} resolver 这个函数的返回值作为缓存的 key
 * @returns {Function} 返回缓存化后的函数
 */
export function memoize(
  fn: (...args: any) => any,
  resolver?: (...args: any) => any
): ((...args: any) => any) & { cache: WeakMap<object, any> } {
  const memoized = (...args: any[]) => {
    /** 有resolver则取resolver的返回值,否则去第一个参数 */
    const key = resolver ? resolver.apply(resolver, args) : args[0];

    const cache = memoized.cache;
    /** 缓存中有的话则直接返回结果 */
    if (cache.has(key)) return cache.get(key);

    /** 调用缓存的函数 */
    const result = fn.apply(fn, args);

    /** 将缓存函数存放在缓存中 */
    memoized.cache = cache.set(key, result) || cache;
    return result;
  };

  /** 定义cache类型 */
  memoized.cache = new WeakMap();

  return memoized;
}

使用例子:

import { memoize } from "moyan-utils"

var object = { a: 1, b: 2 };
var other = { c: 3, d: 4 };

var values = memoize(item => Object.values(item))
values(object) => [ 1, 2 ]
values(other) => [ 3, 4 ]

object.a = 2;
values(object) => [ 1, 2 ]

values.cache.set(object, ['a', 'b']);
values(object) => [ 'a', 'b' ]

数学计算

getRandomColor

用于获取随机颜色,返回值是一个16进制的颜色值

代码实现:

/**
 * 获取随机颜色
 * @returns {string} 16进制的颜色值
 */
export function getRandomColor() {
  return `#${Math.random()
    .toString(16)
    .slice(2, 8)}`;
}

使用例子:

import { getRandomColor } from "moyan-utils"

getRandomColor() => #3617ad

结尾营业

看官都看到这了,如果觉得不错,可不可以不吝啬你的小手手帮忙点个关注或者点个赞

711115f2517eae5e.gif