伸手请间接跳到【办法合集】~

列表 List(包含数组和 Set 或者各种类数组构造都能够是列表)数据的作为系列数据的载体能够说是随处可见了,必然在我的项目开发中是少不了对列表的解决,或多或少的也会有对列表解决办法的封装,这次咱们就来看看有那些常见的列表处理函数。

如果你感觉文章对你有所帮忙,心愿你能够慷慨解囊地给一个赞~

List 构造转 Map 构造

这个能够说是最惯例的,也是最频繁的解决了,当咱们有一系列对象时,常常会遇到依据对象的 id 来查找对应的对象,当列表比拟大或者查找次数比拟多的时候,间接应用 Array.find 来查找老本就会很高,于是将其转成 id 作为 key,对象自身作为 vulue 的 Map 构造了。

// prop 指定应用哪个属性的值作为 keyfunction transformListToMap<T = any>(list: T[], prop: keyof T) {  const map = {} as Record<string, T>  if (!prop) return map  list.forEach(item => {    // 这里丢到 String 里躲避一下 ts 的类型限度    map[String(item[prop])] = item  })  return map}

不过咋一看,这办法如同薄弱的一些,不能笼罩一些绝对简单的状况。

比方当须要一些组合值或者计算值作为 key 时,那单传一个 prop 是不能满足状况的。

再比方,当须要作为 value 的局部不是对象自身,而是一些特定的属性或者一些属性的组合或计算,那显然目前的参数也是无奈反对的。

于是咱们再加亿点细节,欠缺一下这个函数:

// 这是上一期写的 is 系列函数,在文章最底部有链接import { isDefined, isFunction } from './is'// 第二个参数同时反对传入一个函数,以反对返回任意解决的值作为 key// 减少第三个参数,拓展反对传入一个函数以解决任意的值作为 valuefunction transformListToMap<T = any, K = T>(  list: T[],  prop: keyof T | ((item: T) => any),  accessor: (item: T) => K = v => v as any): Record<string, K> {  const map = {} as Record<string, any>  if (!isDefined(prop)) return map  // 对立解决成读取函数  const propAccessor = isFunction(prop) ? prop : (item: T) => item[prop]  list.forEach(item => {    const key = propAccessor(item)    // 避免传入不标准函数呈现 null 或 undefined,让其静默生效    if (isDefined(key)) {      map[key] = accessor(item)    }  })  return map}

移除 List 中的特定元素

我先贴一段代码,我置信大伙应该没少写过:

const list: any[] = [/* ... */]const removedId = 'removedId'const index = list.findIndex(item => item.id === removedId)if (index !== -1) {  list.splice(index, 1)}

没错,依据条件删除列表中的特定元素也是很常见的需要了,于是咱们也能够来封装一下:

function removeArrayItem<T = any>(  array: T[],  item: T | ((item: T) => boolean), // 老样子反对传入一个函数适配简单状况  isFn = false // 用来批示列表里的元素是否是函数,以适配极少数状况): T | null {  let index = -1  if (isFn || typeof item !== 'function') {    index = array.findIndex(current => current === item)  } else {    index = array.findIndex(item as (item: T) => boolean)  }  if (~index) {    return array.splice(index, 1)[0]  }  return null}

不过有时候,咱们可能须要同时删除多个元素,那下面的办法是无奈笼罩的,于是咱们还须要再革新一下:

// 在解决上,会间接操作源列表,并返回被移除的元素汇合function removeArrayItems<T = any>(  array: T[],  items: T | T[] | ((item: T) => boolean),  isFn = false): T[] {  const multiple = Array.isArray(items)  // 针对删除单个元素独自解决  if (!multiple && (isFn || typeof items !== 'function')) {    const index = array.findIndex(current => current === items)    if (~index) {      return array.splice(index, 1)    }  } else {    let filterFn: (item: T) => boolean    if (multiple) {      const removedSet = new Set(items)      filterFn = item => removedSet.has(item)    } else {      filterFn = items as (item: T) => boolean    }    // 浅克隆源列表,用来遍历解决    const originArray = Array.from(array)    const removedItems: T[] = []    // 用源列表来贮存删除后的后果以达到间接操作源列表的目标    array.length = 0    originArray.forEach(item => (filterFn(item) ? removedItems : array).push(item))    return removedItems  }  return []}

函数的后半局部的解决可能有一些形象,能够缓缓屡一下。

这个函数尽管涵盖了多元素删除的状况,不过当应用自定义函数来进行删除时,可能本来只是心愿删除一个元素,但却会对整个列表进行残缺的遍历,从而损失了一些性能。

对 List 中的元素进行归类(GroupBy)

例如有上面这样一组数据:

const list = [  { type: 'a', name: 'x', count: 10 },  { type: 'a', name: 'y', count: 11 },  { type: 'a', name: 'x', count: 12 },  { type: 'a', name: 'y', count: 13 },  { type: 'b', name: 'x', count: 14 },  { type: 'b', name: 'y', count: 15 }]

当初须要针对同 type 且同 name 的数量进行求和,那这里就会须要咱们把数据依照 typename 两个属性进行归类,也就是很经典的 GroupBy 问题了。

其实第一个案例的将 List 转 Map 构造实质也是一个 GroupBy 问题,只不过是最简略的一维归类。

当然如果咱们晓得只会依据两个属性进行归类的话,间接用一个两层的 Map 来贮存后果是没问题的:

const record = {}arr.forEach(({ type, name, count }) => {  if (!record[type]) {    record[type] = {}  }  const typeRecord = record[type]  if (!typeRecord[name]) {    typeRecord[name] = 0  }  typeRecord[name] += count})record.a.x // 22

不过咱们封装通用的工具函数,必定是要思考尽量笼罩可能呈现的状况的(十倍准则),所以咱们出发点是要反对有限层级的分组(只有内存够用),这里就间接上齐全体代码了:

function groupByProps<T = any>(  list: T[],  // 能够传入一个数组按程序指定要 groupBy 的属性  props: Array<string | ((item: T) => any)> | string | ((item: T) => any) = []) {  // 如果传入了单个属性或者函数,先对立解决成数组  if (typeof props === 'string' || typeof props === 'function') {    props = [props]  }  const propCount = props.length  const zipData: Record<string, any> = {}  for (const item of list) {    // 须要一个变量用来记录以后属性对应的分组层级的 record 对象    // 这里的类型推断须要额定定义不少变量,省事来个 any    let data: any    for (let i = 0; i < propCount; ++i) {      const isLast = i === propCount - 1      const prop = props[i]      const value = typeof prop === 'function' ? prop(item) : item[prop as keyof T]      if (!data) {        if (!zipData[value]) {          // 如果到最初一层时,应该初始化一个数组来贮存分组后的后果          zipData[value] = isLast ? [] : {}        }        data = zipData[value]      } else {        if (!data[value]) {          data[value] = isLast ? [] : {}        }        data = data[value]      }    }    data.push(item)  }  return zipData}
这个函数返回后果的类型推断目前没想到特地好的方法,只能先用 Record<string, any> 解决。

依据条件对 List 的元素进行排序

这是这次的最初一个函数了(并不是),也是一个跟高频的场景。

依据我集体以往的教训,凡是遇到须要用表格展现数据的场合,都会呈现依据某列对数据进行排序的需要。

对于只针对繁多属性的排序,我置信大家应该倒着写都能写进去了,对于一个通用函数当然是须要反对多列的排序了(作为最初一个函数,我间接上残缺代码给大家本人读一读):

import { isObject } from './is'// 反对细粒度定制某个属性的排序规定interface SortOptions<T = string> {  key: T,  method?: (prev: any, next: any) => number, // 排序的办法  accessor?: (...args: any[]) => any, // 读取属性的办法  type?: 'asc' | 'desc',  params?: any[] // 传入读取器的额定参数}// 默认的排序办法const defaultSortMethod = (prev: any, next: any) => {  if (Number.isNaN(Number(prev) - Number(next))) {    return String(prev).localeCompare(next)  }  return prev - next}function sortByProps<T = any>(  list: T[],  props: keyof T | SortOptions<keyof T> | (keyof T | SortOptions<keyof T>)[]) {  if (    !list.sort ||    (isObject<SortOptions>(props) && !props.key) ||    !(props as string | SortOptions[]).length  ) {    return list  }  const sortedList = Array.from(list)  if (!Array.isArray(props)) {    props = [props]  }  const formattedProps = props    .map(      value =>        (typeof value === 'string'          ? {              key: value,              method: defaultSortMethod,              type: 'asc'            }          : value) as SortOptions<keyof T>    )    .map(value => {      if (typeof value.accessor !== 'function') {        value.accessor = (data: T) => data[value.key]      }      if (typeof value.method !== 'function') {        value.method = defaultSortMethod      }      value.params = Array.isArray(value.params) ? value.params : []      return value as Required<SortOptions>    })  sortedList.sort((prev, next) => {    let lastResult = 0    for (const prop of formattedProps) {      const { method, type, accessor, params } = prop      const desc = type === 'desc'      const result = method(accessor(prev, ...params), accessor(next, ...params))      lastResult = desc ? -result : result      // 若不为0则无需进行下一层排序      if (lastResult) break    }    return lastResult  })  return sortedList}

List 构造与 Tree 构造的互转

这里援用一下我在两年多前的一篇文章:js将扁平构造数据转换为树形构造

外面解析了将列表数据转树形构造的几种形式,不过是 js 写的,最初的合集会贴上 ts 版本。

而后在合集里会付上将树形构造展平成列表构造的办法,采纳的是循环取代递归的形式,树展平的应用场景绝对较少,就不细说了。

办法合集

没有粗疏校对,如果有一丢丢小错自行修复一下~
import { isDefined, isObject, isFunction } from './is'/** * 依据数组元素中某个或多个属性的值转换为映射 * @param list - 须要被转换的数组 * @param prop - 须要被转换的属性或提供一个读取办法 * @param accessor - 映射的值的读取办法,默认返回元素自身 */export function transformListToMap<T = any, K = T>(  list: T[],  prop: keyof T | ((item: T) => any),  accessor: (item: T) => K = v => v as any): Record<string, K> {  const map = {} as Record<string, any>  if (!isDefined(prop)) return map  const propAccessor = isFunction(prop) ? prop : (item: T) => item[prop]  list.forEach(item => {    const key = propAccessor(item)    if (isDefined(key)) {      map[key] = accessor(item)    }  })  return map}/** * 移除数组中的某个元素 * @param array - 须要被移除元素的数组 * @param item - 须要被移除的元素, 或一个查找办法,如果元素为函数时则须要做一层简略包装 * @param isFn - 标记数组的元素是否为函数 */export function removeArrayItem<T = any>(  array: T[],  item: T | ((item: T) => boolean),  isFn = false): T | null {  let index = -1  if (isFn || typeof item !== 'function') {    index = array.findIndex(current => current === item)  } else {    index = array.findIndex(item as (item: T) => boolean)  }  if (~index) {    return array.splice(index, 1)[0]  }  return null}/** * 移除数组中的某个或多个元素 * @param array - 须要被移除元素的数组 * @param items - 须要被移除的元素, 或一个查找办法 * @param isFn - 标记数组的元素是否为函数 */function removeArrayItems<T = any>(  array: T[],  items: T | T[] | ((item: T) => boolean),  isFn = false): T[] {  const multiple = Array.isArray(items)  if (!multiple && (isFn || typeof items !== 'function')) {    const index = array.findIndex(current => current === items)    if (~index) {      return array.splice(index, 1)    }  } else {    let filterFn: (item: T) => boolean    if (multiple) {      const removedSet = new Set(items)      filterFn = item => removedSet.has(item)    } else {      filterFn = items as (item: T) => boolean    }    const originArray = Array.from(array)    const removedItems: T[] = []    array.length = 0    originArray.forEach(item => (filterFn(item) ? removedItems : array).push(item))    return removedItems  }  return []}/** * 依照肯定程序的属性对数据进行分组 * @param list - 须要分数的数据 * @param props - 须要按程序分组的属性 */export function groupByProps<T = any>(  list: T[],  props: Array<string | ((item: T) => any)> | string | ((item: T) => any) = []): Record<string, T[]> {  if (typeof props === 'string' || typeof props === 'function') {    props = [props]  }  const propCount = props.length  const zipData: Record<string, any> = {}  for (const item of list) {    let data    for (let i = 0; i < propCount; ++i) {      const isLast = i === propCount - 1      const prop = props[i]      const value = typeof prop === 'function' ? prop(item) : item[prop as keyof T]      if (!data) {        if (!zipData[value]) {          zipData[value] = isLast ? [] : {}        }        data = zipData[value]      } else {        if (!data[value]) {          data[value] = isLast ? [] : {}        }        data = data[value]      }    }    data.push(item)  }  return zipData}export interface TreeOptions<T = string> {  keyField?: T,  childField?: T,  parentField?: T,  rootId?: any}/** * 转换扁平构造为树形构造 * @param list - 须要转换的扁平数据 * @param options - 转化配置项 */export function transformTree<T = any>(list: T[], options: TreeOptions<keyof T> = {}) {  const {    keyField = 'id' as keyof T,    childField = 'children' as keyof T,    parentField = 'parent' as keyof T,    rootId = null  } = options  const hasRootId = isDefined(rootId) && rootId !== ''  const tree: T[] = []  const record = new Map<T[keyof T], T[]>()  for (let i = 0, len = list.length; i < len; ++i) {    const item = list[i]    const id = item[keyField]    if (hasRootId ? id === rootId : !id) {      continue    }    if (record.has(id)) {      (item as any)[childField] = record.get(id)!    } else {      (item as any)[childField] = []      record.set(id, (item as any)[childField])    }    if (item[parentField] && (!hasRootId || item[parentField] !== rootId)) {      const parentId = item[parentField]      if (!record.has(parentId)) {        record.set(parentId, [])      }      record.get(parentId)!.push(item)    } else {      tree.push(item)    }  }  return tree}/** * 转换树形构造为扁平构造 * @param tree - 须要转换的树形数据 * @param options - 转化配置项 */export function flatTree<T = any>(tree: T[], options: TreeOptions<keyof T> = {}) {  const {    keyField = 'id' as keyof T,    childField = 'children' as keyof T,    parentField = 'parent' as keyof T,    rootId = null  } = options  const hasRootId = isDefined(rootId) && rootId !== ''  const list: T[] = []  const loop = [...tree]  let idCount = 1  while (loop.length) {    const item = loop.shift()!    let id    let children: any[] = []    const childrenValue = item[childField]    if (Array.isArray(childrenValue) && childrenValue.length) {      children = childrenValue    }    if (item[keyField]) {      id = item[keyField]    } else {      id = idCount++    }    if (hasRootId ? item[parentField] === rootId : !item[parentField]) {      (item as any)[parentField] = rootId    }    for (let i = 0, len = children.length; i < len; ++i) {      const child = children[i]      child[parentField] = id      loop.push(child)    }    list.push(item)  }  return list}export interface SortOptions<T = string> {  key: T,  method?: (prev: any, next: any) => number,  accessor?: (...args: any[]) => any,  type?: 'asc' | 'desc',  params?: any[] // 传入读取器的额定参数}const defaultSortMethod = (prev: any, next: any) => {  if (Number.isNaN(Number(prev) - Number(next))) {    return String(prev).localeCompare(next)  }  return prev - next}/** * 依据依赖的属性逐层排序 * @param list - 须要排序的数组 * @param props - 排序依赖的属性 key-属性名 method-排序办法 accessor-数据获取办法 type-升降序 */export function sortByProps<T = any>(  list: T[],  props: keyof T | SortOptions<keyof T> | (keyof T | SortOptions<keyof T>)[]) {  if (    !list.sort ||    (isObject<SortOptions>(props) && !props.key) ||    !(props as string | SortOptions[]).length  ) {    return list  }  const sortedList = Array.from(list)  if (!Array.isArray(props)) {    props = [props]  }  const formattedProps = props    .map(      value =>        (typeof value === 'string'          ? {              key: value,              method: defaultSortMethod,              type: 'asc'            }          : value) as SortOptions<keyof T>    )    .map(value => {      if (typeof value.accessor !== 'function') {        value.accessor = (data: T) => data[value.key]      }      if (typeof value.method !== 'function') {        value.method = defaultSortMethod      }      value.params = Array.isArray(value.params) ? value.params : []      return value as Required<SortOptions>    })  sortedList.sort((prev, next) => {    let lastResult = 0    for (const prop of formattedProps) {      const { method, type, accessor, params } = prop      const desc = type === 'desc'      const result = method(accessor(prev, ...params), accessor(next, ...params))      lastResult = desc ? -result : result      // 若不为0则无需进行下一层排序      if (lastResult) break    }    return lastResult  })  return sortedList}

往期传送门

【封装小技巧】is 系列办法的封装