【源码学习】Radash(三)Array 上
Array
Radash 使用 TypeScript 编写,提供开箱即用的完整功能。 大多数 Radash 函数都是确定性 和 纯函数(Pure Function)
import { isArray, isFunction } from './typed'array.ts 引入了 isArray 和 isFunction 两个方法用于数组类型的判断。
alphabetical
按属性的字母顺序对对象数组进行排序。
- 基本用法
给定一个对象数组和一个用于确定用于排序的属性的回调函数,返回一个新数组,其中对象按字母顺序排序。第三个可选参数允许您按降序而不是默认的升序排序。对于数字排序,请参阅排序函数。
import { alphabetical } from 'radash'
const gods = [ { name: 'Ra', power: 100 }, { name: 'Zeus', power: 98 }, { name: 'Loki', power: 72 }, { name: 'Vishnu', power: 100 },]
alphabetical(gods, (g) => g.name)// => [Loki, Ra, Vishnu, Zeus]
alphabetical(gods, (g) => g.name, 'desc')// => [Zeus, Vishnu, Ra, Loki]- 源码解析
函数的实现原理是将输入的数组 array 进行排序,然后返回一个新的排序后的数组。同时,函数还支持指定排序的顺序为升序(‘asc’)或降序(‘desc’)。
// 该函数接受一个readonly T[]类型的array// 和一个(item: T) => string类型的getter函数,// 以及一个默认值为'asc'的dir参数export const alphabetical = <T>( array: readonly T[], getter: (item: T) => string, dir: 'asc' | 'desc' = 'asc') => { // 检查array是否为空,如果为空,则返回一个空数组 if (!array) return [] // 定义两个比较函数asc和dsc,分别用于实现升序和降序的排序 const asc = (a: T, b: T) => `${getter(a)}`.localeCompare(getter(b)) const dsc = (a: T, b: T) => `${getter(b)}`.localeCompare(getter(a)) return // 使用array.slice()方法创建一个新数组newArray,该数组浅拷贝原始数组array array .slice() // 使用sort方法对newArray进行排序 // 排序的比较函数为dir === 'desc' ? dsc : asc, // 即如果dir为'desc',则使用dsc比较函数,否则使用asc比较函数 // 最后返回排序后的newArray .sort(dir === 'desc' ? dsc : asc)}- 注意事项
- 由于函数使用了 readonly 修饰符,这意味着
不能直接修改输入数组 array(这里鼓励使用不可变数据,官方倡导函数式编程)。为了避免混淆,建议使用 array.slice() 创建一个新的数组进行排序。后面的 readonly 同,就不赘述了 - 函数 getter 函数用于获取每个元素的排序关键字,这对于实现自定义排序非常灵活。例如,如果需要根据某个属性排序,可以使用
getter = (item: T) => item.sortKey。 - 函数默认使用升序(‘asc’)进行排序,如果需要使用降序(‘desc’),只需在调用时指定 dir 参数即可,例如
alphabetical(array, getter, 'desc')。
boil
将项目列表减少到一项
- 基本用法
给定一个项目数组,返回赢得比较条件的最终项目。对于更复杂的最小/最大有用。
import { boil } from 'radash'
const gods = [ { name: 'Ra', power: 100 }, { name: 'Zeus', power: 98 }, { name: 'Loki', power: 72 },]
boil(gods, (a, b) => (a.power > b.power ? a : b))// => { name: 'Ra', power: 100 }- 源码解析
export const boil = <T>( // 同上,就不赘述了。数组不可更改 array: readonly T[], // compareFunc 是一个比较函数。这里作为参数,并返回一个T类型的值 compareFunc: (a: T, b: T) => T) => { // 首先检查array是否为空;如果为空,则返回null if (!array || (array.length ?? 0) === 0) return null // 使用reduce方法结合compareFunc函数来计算数组中的所有元素的和 return array.reduce(compareFunc) // 这里你可能会好奇, 默认值是什么呢? // reduce方法会从左到右遍历数组,每次将当前元素 a 与上一个元素 b 传入compareFunc函数, // 并将结果与下一个元素继续传入compareFunc函数,直到遍历完整个数组。 // 我们看示例。 比较函数 (a, b) => a.power > b.power // 表示如果a的power大于b的power,则返回a,否则返回b。 // 100 > 98, 所以 { name: 'Ra', power: 100 } 保留了,然后 // { name: 'Ra', power: 100 } 保留, 继续与 { name: 'Loki', power: 72 } 作比较 // { name: 'Ra', power: 100 } 仍保留,遍历结束,作为最终的返回值}cluster
将列表拆分为多个给定大小的列表.
- 基本用法
给定一个项目数组和所需的列表大小 (n),返回一个数组数组。每个包含 n(列表大小)项的子数组尽可能均匀地分割。
import { cluster } from 'radash'
const gods = [ 'Ra', 'Zeus', 'Loki', 'Vishnu', 'Icarus', 'Osiris', 'Thor', 'Apollo', 'Artemis', 'Athena',]
cluster(gods, 3)// => [// [ 'Ra', 'Zeus', 'Loki' ],// [ 'Vishnu', 'Icarus', 'Osiris' ],// ['Thor', 'Apollo', 'Artemis'],// ['Athena']// ]- 源码解析
// 将一个数组list按照指定的size大小进行分组,返回一个由分组结果组成的二维数组export const cluster = <T>(list: readonly T[], size: number = 2): T[][] => { // 计算分组后的集群数量 clusterCount const clusterCount = Math.ceil(list.length / size) // 使用Array.fill()方法创建一个包含clusterCount个null(用于占位)的数组clusters return ( new Array(clusterCount) .fill(null) // 遍历clusters数组,为每个集群分配一组数据 .map((_c: null, i: number) => { // 使用slice()方法从list中切出当前集群的数据, // 切片的起始位置为i * size,结束位置为i * size + size return list.slice(i * size, i * size + size) // 将切出的数据添加到当前集群的位置, // _c 就是一个集群,所包含的就是本次slice() 方法切出的数据(理解为前端分页就成) }) )}counting
创建一个包含项目出现次数的对象
- 基本用法
给定一个对象数组和一个身份回调函数来确定如何识别每个对象。返回一个对象,其中键是回调返回的 id 值,每个值都是一个整数,表示该 id 出现了多少次。
import { counting } from 'radash'
const gods = [ { name: 'Ra', culture: 'egypt' }, { name: 'Zeus', culture: 'greek' }, { name: 'Loki', culture: 'greek' },]
counting(gods, (g) => g.culture) // => { egypt: 1, greek: 2 }- 源码分析
这个函数的目的是计算给定列表中每个元素的身份表示(通过 identity 函数)的计数
export const counting = <T, TId extends string | number | symbol>( list: readonly T[], // identity是一个类型参数,它可以是字符串、数字或符号 identity: (item: T) => TId): Record<TId, number> => { // 我们检查list是否为空,如果为空,则返回一个空的记录,其中键是TId类型,值是number类型 if (!list) return {} as Record<TId, number>
return list.reduce((acc, item) => { // 使用identity函数将item转换为TId类型 // 鼠标移动到 id -> TId extends string | number | symbol const id = identity(item) // 检查acc中是否已经存在id,如果存在,则将id对应的值加1,否则将id设置为1 acc[id] = (acc[id] ?? 0) + 1 // 继续看示例,一开始也就是 {},没有 egypt 所以 // acc[egypt] = 0,那么第一次遍历 acc[egypt] = 0 + 1 // 这里就不展开了,打个断点调试就会很清楚了 // 最终得到 => { egypt: 1, greek: 2 } return acc }, {} as Record<TId, number>) // 默认值为 {}}diff
创建两个数组之间的差异的数组
- 基本用法
给定两个数组,返回第一个数组中存在但第二个数组中不存在的所有项目的数组。
import { diff } from 'radash'
const oldWorldGods = ['ra', 'zeus']const newWorldGods = ['vishnu', 'zeus']
diff(oldWorldGods, newWorldGods) // => ['ra']- 源码解析
这个函数可以用于实现两个不同集合的差异操作
export const diff = <T>( root: readonly T[], other: readonly T[], // 见下面的identity拆解. 理清楚了再回过头来看就简单多了 identity: (item: T) => string | number | symbol = (t: T) => t as unknown as string | number | symbol): T[] => { // 边界判断,如果两个数组都为空,则返回空数组 if (!root?.length && !other?.length) return [] // 如果root为空,则返回other if (root?.length === undefined) return [...other] // 如果other为空,则返回root if (!other?.length) return [...root]
// 构建一个bKeys对象:用 identity 值作为键的布尔值对象 const bKeys = other.reduce((acc, item) => { // identity('vishnu') -> 'vishnu' // 打断点代入一下: // bKeys = { vishnu: true, zeus: true } acc[identity(item)] = true return acc }, {} as Record<string | number | symbol, boolean>)
// 使用 filter 方法遍历 root 集合,对于每个元素 a, // 如果 bKeys 中不存在 identity(a) 作为键,则将 a 添加到新集合中 // 接着上面的代码继续代入: // !bKeys['ra'] -> true // !bKeys['zeus'] -> false // 所以最终返回的结果是 ['ra'] return root.filter((a) => !bKeys[identity(a)])}说实话,刚看到这里也卡了阵 - - 不能逃避,果断格式化,将代码再细分、拆解,果然这样一弄就轻松多了。
// diff函数接受一个类型为 T// 疑问:这里 是 T,返回的是 T[] 啊?不一样啊?// 我们看示例,传的 其实是 params: { o: string[], n: string[] },// 最终返回的是 string[],所以确实是 T[] 没毛病
// 该函数实现基于 TypeScript 的内置类型转换,// 它会将传入的值转换为 string、number 或 symbol 类型之一const diff = <T>(identity: (item: T) => ( string | number | symbol ) = (t: T) => (t as unknown) as (string | number | symbol) // 这里将变量 t 转换为未知类型(unknown) // 然后强制将其转换为 string、number 或 symbol 之一 // 这样可以确保函数返回的类型是预期的类型之一): T[] {}注:在实际应用中,应该根据具体情况来判断是否需要这种类型的转换,并确保有适当的类型检查和错误处理机制。
flat
将数组的数组展平为一维。
印象中最新 ESNext 貌似增加了该方法,这里也可学习下。
- 基本用法
给定一个包含许多数组的数组,返回一个新数组,其中子级的所有项目都出现在顶层。
import { flat } from 'radash'
const gods = [['ra', 'loki'], ['zeus']]flat(gods) // => [ra, loki, zeus]- 源码解析
通过 reduce 函数遍历多维数组的每一层,将当前层的元素推入 acc 数组,然后返回 acc 数组,从而实现拍平的效果。
export const flat = <T>(lists: readonly T[][]): T[] => { return lists.reduce((acc, list) => { // 在reduce函数的回调函数中,将当前层的元素推入acc数组,然后返回acc数组 acc.push(...list) // 遍历lists数组完成后,acc数组中包含了所有层的元素,即为展平后的数组 return acc }, []) // 初始化acc数组为空数组}注意:
- 请确保 lists 是一个有效的多维数组,否则在 reduce 函数中可能会出现错误。
- 由于 reduce 函数的回调函数会频繁地创建新的数组,因此在性能上可能不是最优的解决方案。
如果对性能有严格要求,可以考虑使用其他数据结构或算法
fork
按条件将数组拆分为两个数组
- 基本用法
给定一个项目数组和一个条件,返回两个数组,其中第一个包含通过条件的所有项目,第二个包含未通过条件的所有项目。
import { fork } from 'radash'
const gods = [ { name: 'Ra', power: 100 }, { name: 'Zeus', power: 98 }, { name: 'Loki', power: 72 }, { name: 'Vishnu', power: 100 },]
const [finalGods, lesserGods] = fork(gods, (f) => f.power > 90)// [[ra, vishnu, zues], [loki]]- 源码解析
export const fork = <T>( // 由于使用了readonly T[]类型,这意味着list在函数外部无法修改 list: readonly T[], condition: (item: T) => boolean): [T[], T[]] => { // 非空判断,确定返回类型 if (!list) return [[], []] return list.reduce( (acc, item) => { // acc 默认值为 [[], []] // 这里解构相当于 a = [], b = [] const [a, b] = acc // 将list中的每个元素依次应用condition函数 // 根据结果将元素添加到不同的数组中 if (condition(item)) { return [[...a, item], b] } else { return [a, [...b, item]] } }, // 这里使用了隐式的类型转换as [T[], T[]] // 这本身是一个安全的类型转换 // 为了提高代码的可读性,可以显式地声明返回类型的类型 [[], []] as [T[], T[]] )}group
对一组项目进行排序
- 基本用法
给定一个项目数组,group 将构建一个对象,其中每个键都是属于该组的项目的数组。一般来说,这对于对数组进行分类很有用。
import { group } from 'radash'
const fish = [ { name: 'Marlin', source: 'ocean' }, { name: 'Bass', source: 'lake' }, { name: 'Trout', source: 'lake' },]
const fishBySource = group(fish, (f) => f.source)// => { ocean: [marlin], lake: [bass, trout] }- 源码解析
// 泛型解析:T 占位,Key 为三个基本类型之一export const group = <T, Key extends string | number | symbol>( // 参数解析: // array: 一个只读的 T类型组成的数组(只读 是为了防止在运行时修改数组) array: readonly T[], // getGroupId: 是个函数,该函数接受一个T类型的参数,并返回一个Key类型的值 getGroupId: (item: T) => Key // 返回一个对象,该对象的键是 Key 类型,值是 T 类型的数组,Partial将对象键都变为可选): Partial<Record<Key, T[]>> => { return array.reduce((acc, item) => { // 通过reduce方法遍历数组,对于每个元素,调用getGroupId函数获取其分组ID const groupId = getGroupId(item) // 然后根据分组ID在 accumulator 中创建或更新一个数组, if (!acc[groupId]) acc[groupId] = [] // 并将当前元素添加到该数组中,最后返回accumulator。 acc[groupId].push(item) return acc }, {} as Record<Key, T[]>) // 使用 {} as Record<Key, T[]> 来初始化 accumulator // 这是 reduce的第二个参数,代表初始值 ,用来存储每次迭代的结果}::: warning 注意: 这个函数只会创建或更新分组,而不是删除分组。如果需要实现删除分组的功能,可以在 accumulator 中检查对应的分组是否为空,如果为空则删除该分组 :::
intersects
确定两个数组是否有公共项
- 基本用法
给定两个项目数组,如果两个数组中都存在任何项目,则返回 true。
import { intersects } from 'radash'
const oceanFish = ['tuna', 'tarpon']const lakeFish = ['bass', 'trout']
intersects(oceanFish, lakeFish) // => false
const brackishFish = ['tarpon', 'snook']
intersects(oceanFish, brackishFish) // => true- 源码分析
函数的实现原理是将 listB 中的元素映射为一个键值对,然后检查 listA 中是否存在具有相同键的元素。如果存在,则两个列表相交;否则,不相交。
export const intersects = <T, K extends string | number | symbol>( listA: readonly T[], listB: readonly T[], // K是一个扩展类型,表示转换函数返回的类型。 identity?: (t: T) => K): boolean => { // 数组空判断,为空直接返false if (!listA || !listB) return false
// 如果identity函数未定义,则使用默认的转换函数 // 该函数将元素转换为其自身的未知类型。 const ident = identity ?? ((x: T) => x as unknown as K)
// 使用reduce方法将listB中的元素映射为一个键值对,生成一个字典dictB const dictB = listB.reduce((acc, item) => { acc[ident(item)] = true return acc }, {} as Record<string | number | symbol, boolean>) // 使用some方法检查listA中是否存在与dictB中的键相等的元素。 // 如果存在,则两个列表相交,返回true;否则,不相交,返回false。 return listA.some((value) => dictB[ident(value)])}iterate
迭代回调 n 次。该函数可以用于对一些数据进行迭代处理,例如对数组进行去重、排序等操作。
- 基本用法
有点像 forEach 遇到 reduce。对于运行函数 n 次以生成值很有用。 _.iterate 函数采用计数(运行回调的次数)、回调函数和初始值。回调作为减速器运行多次,然后返回累积值。
import { iterate } from 'radash'
const value = iterate( 4, (acc, idx) => { return acc + idx }, 0) // => 10- 源码解析
实现原理是使用一个循环,每次迭代都调用 func 函数,并将结果赋值给 value 变量。循环将持续进行 count 次。最后返回 value 变量
export const iterate = <T>( // 循环将持续进行count次 count: number, // func函数应该接收两个参数:当前值(currentValue)和当前迭代次数(iteration) func: (currentValue: T, iteration: number) => T, // initValue参数是初始值 initValue: T) => { let value = initValue for (let i = 1; i <= count; i++) { value = func(value, i) } return value}last
获取列表中的最后一项
- 基本用法
给定一个项目数组,返回最后一个项目,如果不存在项目,则返回默认值。
import { last } from 'radash'
const fish = ['marlin', 'bass', 'trout']const lastFish = last(fish) // => 'trout'const lastItem = last([], 'bass') // => 'bass'我们平时都会写 arr[arr.length - 1] 那么看下 Radash 源码又有什么不一样吧?
- 源码解析
export const last = <T>( // readonly 不可变数组 array: readonly T[], // 默认值,可选 defaultValue: T | null | undefined = undefined) => { // 这里用到了 ?. 操作符 // 先检查array是否为null或undefined,或者是否是一个空数组 return array?.length > 0 ? // 如果 array 存在,则返回 array[array.length - 1] array[array.length - 1] : // 如果 array 不存在,则返回默认值 defaultValue // 如果默认值不存在,则返回 undefined}使用可选类型可以避免因为检查空数组导致的错误。对比我们平时写的代码多了健全的类型判断,只要注意边界判断,大家都可以写出高质量的代码。
max
从数组中获取最大的项
- 基本用法
给定一个项数组和一个获取每个项值的函数,返回具有最大值的项。在内部使用 _.boil。
import { max } from 'radash'
const fish = [ { name: 'Marlin', weight: 105, source: 'ocean', }, { name: 'Bass', weight: 8, source: 'lake', }, { name: 'Trout', weight: 13, source: 'lake', },]
max(fish, (f) => f.weight)// => {name: "Marlin", weight: 105, source: "ocean"}min & max
获取数组中最小 / 最大的项(由于实现基本相同,这里放一起说了)
- 基本用法
同上。给定一个项数组和一个获取每个项值的函数,返回具有最小值的项。在内部使用 _.boil。
import { min, max } from 'radash'
const fish = [ { name: 'Marlin', weight: 105, source: 'ocean', }, { name: 'Bass', weight: 8, source: 'lake', }, { name: 'Trout', weight: 13, source: 'lake', },]
min(fish, (f) => f.weight)// => {name: "Bass", weight: 8, source: "lake"}max(fish, (f) => f.weight)// => {name: "Marlin", weight: 105, source: "ocean"}- 源码解析
// 这里是函数重载的运用:// 1. 接受一个readonly [number, ...number[]]类型的数组,返回一个number类型的最小值// 这种类型表示法通常用于表示一个不可变的整数数组export function min(array: readonly [number, ...number[]]): number// 2. 接受一个readonly number[]类型的数组,返回一个number类型的最小值export function min(array: readonly number[]): number | null// 3. 接受一个readonly T[]类型的数组和一个可选的getter函数// 返回一个T类型的最小值。 这允许min函数处理非数字类型的数据export function min<T>( array: readonly T[], getter: (item: T) => number): T | nullexport function min<T>( array: readonly T[], getter?: (item: T) => number): T | null { // 尝试使用getter函数来获取数组中每个元素的值 const get = getter ?? ((v: any) => v) // 如果getter未定义,则使用默认的比较函数((a, b) => a < b) // 我们调用boil函数对数组进行排序,并返回排序后的第一个元素 return boil(array, (a, b) => (get(a) < get(b) ? a : b))}
export function max(array: readonly [number, ...number[]]): numberexport function max(array: readonly number[]): number | nullexport function max<T>( array: readonly T[], getter: (item: T) => number): T | nullexport function max<T>( array: readonly T[], getter?: (item: T) => number): T | null { const get = getter ?? ((v: any) => v) return boil(array, (a, b) => (get(a) > get(b) ? a : b))}merge
合并两个列表,覆盖第一个列表中的项
- 基本用法
给定两个项数组和一个标识函数,返回第一个列表中所有与第二个列表匹配的项。
这个函数可以用于合并两个数组,特别是当需要根据某些规则(由 matcher 函数定义)来匹配和合并数组中的元素时非常有用。例如,可以在合并用户列表时使用这个函数,根据用户名进行匹配。
import { merge } from 'radash'
const gods = [ { name: 'Zeus', power: 92, }, { name: 'Ra', power: 97, },]
const newGods = [ { name: 'Zeus', power: 100, },]
merge(gods, newGods, (f) => f.name)// => [{name: "Zeus" power: 100}, {name: "Ra", power: 97}]使用 lib 经常会看到 mergeOptions 方法,原理差不多,用来合并两个对象,覆盖第一个对象中的项。
- 源码解析
export const merge = <T>( root: readonly T[], // root和others都是只读的数组 others: readonly T[], // matcher是一个函数,用于比较两个对象的相似性 matcher: (item: T) => any) => { // 边界、非空判断:这里至少保证该方法返回一个空数组 if (!others && !root) return [] if (!others) return root if (!root) return [] if (!matcher) return root
// reduce方法中的函数会遍历root数组中的每个元素, // 并将其与others数组中的每个元素进行比较。 return root.reduce((acc, r) => { const matched = others.find((o) => matcher(r) === matcher(o)) // 如果找到了匹配的元素,则将其添加到结果数组中; if (matched) acc.push(matched) // 否则,将当前元素添加到结果数组中。 else acc.push(r)
// 最后,返回合并后的结果数组。 return acc }, [] as T[])}支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!