伸手請直接跳到【方法合集】~
列表 List
(包括陣列和 Set
或者各種類陣列結構都可以是列表)資料的作為系列資料的載體可以說是隨處可見了,必然在專案開發中是少不了對列表的處理,或多或少的也會有對列表處理方法的封裝,這次我們就來看看有那些常見的列表處理函式。
如果你覺得文章對你有所幫助,希望你可以慷慨解囊地給一個贊~
List 結構轉 Map 結構
這個可以說是最常規的,也是最頻繁的處理了,當我們有一系列物件時,經常會遇到根據物件的 id
來查詢對應的物件,當列表比較大或者查詢次數比較多的時候,直接使用 Array.find
來查詢成本就會很高,於是將其轉成 id
作為 key
,物件本身作為 vulue
的 Map 結構了。
// prop 指定使用哪個屬性的值作為 key
function 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
// 增加第三個引數,擴充支援傳入一個函式以處理任意的值作為 value
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)
// 防止傳入不規範函式出現 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
的數量進行求和,那這裡就會需要我們把資料按照 type
和 name
兩個屬性進行歸類,也就是很經典的 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
}
往期傳送門: