一行代码实现18个数组相关的功能函数|TypeScript版
前言
近期整理了一下数组相关的操作函数,一行代码实现一个功能。涵盖了数组的创建、填充、映射、过滤、查找、排序、统计、平均、检查、获取、删除、旋转、唯一等常见操作,可以在实际开发中大大提高代码的简洁性。
有人会说,你这种代码敢用在项目里?你猜会不会被同事骂死?
嘿嘿,那就教你一招,把本文每个功能函数所对应的代码详解,当成注释写上,这样不仅不会被骂,还会被夸牛*,我相信你会回来感谢我的!
以下每个函数均包含详细解释及使用示例,尽情享用吧!!!
上菜
1、交换二维数组(矩阵)的行和列
可视版
可复制版
const transpose = <T,>(matrix: T[][]): T[][] => matrix[0].map((col, i) => matrix.map((row) => row[i]));
代码详解
首先,定义一个泛型函数 transpose
,用于接收一个二维数组 matrix
,并返回转置后的二维数组。该函数的实现使用了两个嵌套的 Array.prototype.map()
方法。
在第一个 map
中,它对第一行(即索引为 0 的行)进行映射,并返回一个数组,其中每个元素为原始矩阵中的一列。这里的 col
表示当前列的元素,而 i
则表示当前列的索引。
接着,在第二个 map
中,它对原始矩阵的每一行进行映射,并根据当前列的索引 i
返回新矩阵中的每一行。具体来说,row[i]
表示原始矩阵中第 i
列的元素,因此它会将每个 row[i]
放入新矩阵的对应行中。
最终,transpose
函数返回转置后的矩阵,即每一行为原始矩阵中的一列的元素的新矩阵。该函数使用了 TypeScript 的泛型 <T>
,以支持处理任意类型的矩阵。
使用示例
transpose([
// [
[1, 2, 3], // [1, 4, 7],
[4, 5, 6], // [2, 5, 8],
[7, 8, 9], // [3, 6, 9],
]); // ]
2、将数组拆分为若干子数组
可视版
可复制版
const chunk = <T,>(arr: T[], size: number): T[][] => (
arr.reduce((acc, e, i) => (i % size ? acc[acc.length - 1].push(e) : acc.push([e]), acc), [] as T[][])
);
代码详解
chunk
函数接收两个参数,一个是待拆分的数组 arr
,另一个是指定的子数组大小 size
。该函数使用了 reduce()
方法来实现拆分操作。reduce()
方法的第一个参数是一个回调函数,该函数接收三个参数:累加器 acc
、当前元素 e
和当前元素的索引 i
。
在回调函数中,它首先使用模运算符(i % size
)来检查当前元素是否应该被放入新的子数组中。如果应该,则将该元素 e
放入当前累加器中的最后一个子数组中(即 acc[acc.length - 1]
),否则将该元素放入一个新的子数组中,并将该子数组放入累加器 acc
中。
最后,chunk
函数返回拆分后的二维数组,其中每个子数组的大小为指定的大小 size
。值得注意的是,该函数使用了 TypeScript 中的类型断言 [] as T[][]
,来将累加器的初始值设置为一个空的二维数组。
使用示例
chunk([1, 2, 3, 4, 5, 6, 7, 8], 3); // [[1, 2, 3], [4, 5, 6], [7, 8]]
chunk([1, 2, 3, 4, 5, 6, 7, 8], 4); // [[1, 2, 3, 4], [5, 6, 7, 8]]
3、根据索引交换数组中的元素
可视版
可复制版
const swapItems = <T,_>(a: T[], i: number, j: number): T[] => (a[i] && a[j] && [...a.slice(0, i), a[j], ...a.slice(i + 1, j), a[i], ...a.slice(j + 1)]) || a;
代码详解
swapItems
函数接收三个参数,一个是待操作的数组 a
,另外两个是待交换的元素的索引 i
和 j
。
该函数使用了条件运算符 &&
和 ||
来处理输入数组为空或待交换的索引越界的情况。当数组中不存在待交换的索引位置时,该函数直接返回原始数组 a
。
否则,该函数使用了 ES6 的扩展语法来创建一个新数组,该新数组包含了原始数组中除了 a[i]
和 a[j]
之外的所有元素,以及将 a[i]
和 a[j]
进行交换后的结果。具体来说,它通过 slice()
方法将原始数组切成三个部分:左侧部分包含索引 i
之前的所有元素,中间部分包含索引 i
和 j
之间的所有元素(不包括 a[i]
和 a[j]
),右侧部分包含索引 j
之后的所有元素。
最后,swapItems
函数返回交换后的新数组,其中 a[i]
和 a[j]
的位置已经被交换。值得注意的是,该函数的第二个泛型参数 _
是 TypeScript 的占位符,表示在实现中并未使用到该参数,用于解决 TypeScript 类型推断时的一些问题。
使用示例
swapItems([1, 2, 3, 4, 5], 1, 4); // [1, 5, 3, 4, 2]
4、两个数组是否完全相等
可视版
可复制版
const isEqual = <T,_>(a: T[], b: T[]): boolean => a.length === b.length && a.every((v, i) => v === b[i]);
代码详解
isEqual
函数接收两个参数,分别是待比较的两个数组 a
和 b
。该函数使用了条件运算符 &&
和 every()
方法来进行比较操作。
首先,该函数比较两个数组的长度是否相等,如果不相等,则直接返回 false
,表示两个数组不相等。如果长度相等,则使用 every()
方法对两个数组中的每个元素进行比较。every()
方法接收一个回调函数作为参数,该回调函数接收两个参数:当前元素的值 v
和当前元素的索引 i
。该回调函数返回一个布尔值,表示当前元素是否与另一个数组中的对应元素相等。
因为 every()
方法会遍历整个数组,只有当所有元素都满足指定条件时,才会返回 true
,因此该函数只有当两个数组的长度相等且所有元素都相等时,才会返回 true
,表示两个数组相等。否则,该函数返回 false
,表示两个数组不相等。
使用示例
isEqual([1, 2, 3], [1, 2, 3]); // true
isEqual([1, 2, 3], [1, '2', 3]); // false
5、两个数组是否完全相等,与顺序无关
可视版
可复制版
const isEqual = <T,_>(a: T[], b: T[]): boolean => JSON.stringify([...(new Set(a))].sort()) === JSON.stringify([...(new Set(b))].sort());
代码详解
与前面介绍的 isEqual
函数不同,该函数使用了 JSON.stringify
方法进行比较操作。
具体来说,该函数接收两个参数,分别是待比较的两个数组 a
和 b
。该函数使用了 TypeScript 的泛型 <T>
,以支持处理任意类型的数组。
该函数的实现过程是先对两个数组去重,然后对去重后的数组进行排序,最后使用 JSON.stringify
方法将排序后的数组转换成字符串进行比较。如果两个数组相等,则它们去重后的排序结果应该是一样的,因此使用 JSON.stringify
进行字符串比较即可。
具体来说,该函数使用了 ES6 的 Set 对象,将数组去重后使用扩展运算符将其转换为数组,并使用 sort()
方法对数组进行排序。然后,将两个排序后的数组分别使用 JSON.stringify
方法转换成字符串,再进行字符串比较,如果相等则返回 true
,表示两个数组相等,否则返回 false
,表示两个数组不相等。
值得注意的是,该方法只适用于比较简单的数组,因为 JSON.stringify
方法不能处理复杂类型的数组,如包含函数或 Symbol 类型的数组。此外,在对数组进行排序时,需要考虑到具体业务逻辑的要求,因为排序后的数组可能会影响到最终的比较结果。
使用示例
isEqual([1, 2, 3], [1, 2, 3]); // true
isEqual([1, 2, 3], [1, 3, 2]); // true
isEqual([1, 2, 3], [1, '2', 3]); // false
6、字符串数组转换成数字数组
可视版
可复制版
const toNumbers = (arr: string[]): number[] => arr .filter((str) => !isNaN(Number(str))) .map(Number);
代码详解
该函数接收一个参数 arr
,类型为字符串数组,表示待转换的字符串数组。
函数实现使用了 filter
方法和 map
方法,对数组中的每个元素进行遍历,先使用 JavaScript 的内置 Number
函数将每个字符串元素转换成数字,然后在 filter
方法中判断该元素是否为 NaN
(即是否无法转换成数字),如果不是 NaN
,则在 map
方法中将其转换成数字,并将所有有效元素的数字数组作为函数的返回值,返回给调用方。
这里使用了两次 Number
函数,一次在 filter
方法中,一次在 map
方法中。在 filter
方法中使用 Number
函数是为了判断每个元素是否能够被正确转换为数字,而在 map
方法中使用 Number
函数是将所有有效元素都转换成数字。
此外,需要注意的是 isNaN
函数的一个特殊性质:如果传入的参数类型不是数字,isNaN
函数会先尝试将其转换为数字,然后再判断是否为 NaN
。因此,代码中的 (str) => !isNaN(Number(str))
实际上相当于 (str) => typeof Number(str) === 'number' && !isNaN(Number(str))
,即先尝试将字符串转换成数字,然后判断转换后的结果是否为 NaN
,如果不是,则说明该字符串可以正确地被转换成数字。
使用示例
toNumbers(['2', '3', '4']); // [2, 3, 4]
7、对数组中的对象按属性计数
可视版
可复制版
const countBy = <T extends Record<string, string>, K extends keyof T>(arr: T[], prop: K): Record<string, number> => (
arr.reduce((prev, curr) => ((prev[curr[prop]] = ++prev[curr[prop]] || 1), prev), {} as Record<string, number>)
);
代码详解
该函数接收两个参数,第一个参数 arr
表示待计数的数组,类型为 T[]
,其中 T
表示一个对象类型,该对象类型中包含若干个属性,其中至少包含一个属性名为 prop
的属性。第二个参数 prop
表示需要对数组中的元素按照该属性进行计数,类型为 K
,其中 K
表示 T
类型中的一个属性名。
函数实现使用了 reduce
方法,对数组中的每个元素进行遍历,将元素的 prop
属性值作为 key,将计数作为 value 存储在一个新的对象中,并将该对象作为函数的返回值返回给调用方。
需要注意的是,在 reduce
方法中的回调函数中,prev
表示上一个元素的计数结果对象,curr
表示当前元素。这里使用了 JavaScript 中的逗号运算符,将递增的计数结果赋值给 prev[curr[prop]]
,然后将 prev
对象作为返回值传递给下一个元素的回调函数。这样,reduce
方法会将所有元素遍历完后得到的最终结果返回给调用方。
此外,该函数使用了 TypeScript 的类型约束,其中 T
表示对象类型,K
表示该对象类型中的一个属性名,通过 extends
关键字来约束 T
必须是包含 prop
属性的对象类型,K
必须是 T
对象类型中的一个属性名。这样,就可以确保在函数中使用 prop
属性时不会出现类型错误。
使用示例
countBy(
[
{ branch: 'audi', model: 'q8', year: '2019' },
{ branch: 'audi', model: 'rs7', year: '2020' },
{ branch: 'ford', model: 'mustang', year: '2019' },
{ branch: 'ford', model: 'explorer', year: '2020' },
{ branch: 'bmw', model: 'x7', year: '2020' },
],
'branch'
);
// { 'audi': 2, 'ford': 2, 'bmw': 1 }
8、统计数组某个元素出现的次数
可视版
可复制版
const countOccurrences = <T,_>(arr: T[], val: T): number => arr.filter((item) => item === val).length;
代码详解
该函数接收两个参数,第一个参数 arr
表示待计数的数组,类型为 T[]
,其中 T
表示数组中元素的类型。第二个参数 val
表示需要计数的元素,类型为 T
。
函数使用了 filter
方法对数组进行遍历,对于数组中的每个元素,将其与目标元素 val
进行比较,若相等则将其保留在数组中,否则过滤掉该元素。最后使用数组的 length
属性返回保留下来的元素个数,即为 val
出现的次数。
使用示例
countOccurrences([2, 1, 3, 3, 2, 3], 2); // 2
countOccurrences(['a', 'b', 'a', 'c', 'a', 'b'], 'a'); // 3
9、统计数组每个元素出现的次数
可视版
可复制版
const countOccurrences = <T extends string | number,>(arr: T[]): Record<T, number> => (
arr.reduce((prev, curr) => ((prev[curr] = ++prev[curr] || 1), prev), {}as Record<T, number>)
);
代码详解
该函数接收一个参数 arr
,表示待计数的数组,类型为 T[]
,其中 T
是字符串或数字类型。该函数的返回值为一个对象,对象的键为数组中的元素,值为该元素在数组中出现的次数。
函数使用了 reduce
方法对数组进行遍历,对于数组中的每个元素,将其作为对象的键,将该键的值加一,表示该元素在数组中出现了一次。需要注意的是,如果该键不存在,需要将该键的值设置为 1。
由于 TypeScript 不支持泛型约束对象的键,因此该函数的类型定义中,将泛型 T
限制为字符串或数字类型,并将返回值的类型定义为一个泛型记录类型,其键为 T
类型,值为 number
类型。
注意,该函数不考虑数组中的元素类型,对于非字符串或数字类型的元素,将会出现类型错误。
使用示例
countOccurrences([2, 1, 3, 3, 2, 3]); // { '1': 1, '2': 2, '3': 3 }
countOccurrences(['a', 'b', 'a', 'c', 'a', 'b']); // { 'a': 3, 'b': 2, 'c': 1 }
10、生成给定范围内的所有整数
可视版
可复制版
const range = (min: number, max: number): number[] => [...Array(max - min + 1).keys()].map((i) => i + min);
代码详解
该函数接收两个参数 min
和 max
,分别表示数字范围的最小值和最大值。函数的返回值为一个数组,其中包含了该数字范围内的所有整数。
函数使用了 ES6 中的 Array.from
方法结合 keys()
方法来生成数字范围内的整数。具体来说,该函数先使用了 Array
构造函数生成一个长度为 max - min + 1
的数组,然后使用 keys()
方法生成了该数组的索引迭代器,最后使用 map()
方法对迭代器进行映射,将迭代器中的索引值加上 min
,得到了数字范围内的所有整数。
这里使用了扩展运算符 ...
将迭代器转换为数组,避免了使用 Array.from
方法。这是因为 keys()
方法生成的迭代器只能迭代索引值,而不是元素值,因此无需使用 Array.from
方法。
注意,该函数不会检查参数 min
和 max
是否为整数,也不会检查参数的大小关系。如果参数不符合要求,将会导致函数返回错误的结果。
使用示例
range(5, 10); // [5, 6, 7, 8, 9, 10]
11、查找数组中与给定数最接近的数字
可视版
可复制版
const closest = (arr: number[], n: number): number => arr.reduce((prev, curr) => (Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev));
代码详解
该函数接受两个参数:一个数字数组 arr
和一个数字 n
。函数返回 arr
中距离 n
最近的数字。具体实现使用了 reduce
方法和三元运算符。
首先,使用 reduce
方法遍历数组 arr
,将其每个元素与 n
比较,返回差值较小的那个元素。具体来说,reduce
的第一个参数是一个函数,它接受两个参数:prev
和 curr
。prev
表示上一次 reduce
执行时的返回值,而 curr
表示当前遍历到的元素。在这个函数中,我们使用三元运算符比较 curr
与 n
的距离和 prev
与 n
的距离的大小,并返回较小距离对应的那个数。最终,reduce
返回 arr
中距离 n
最近的数。
注意,如果 arr
为空数组,reduce
方法将无法执行,此时会返回 undefined
。因此,调用该函数前需要确保 arr
不为空。
使用示例
closest([29, 87, 8, 78, 97, 20, 75, 33, 24, 17], 50); // 33
12、累加数组元素,生成新数组
可视版
可复制版
const accumulate = (arr: number[]): number[] => arr.reduce((a, b, i) => (i === 0 ? [b] : [...a, b + a[i - 1]]), [0]);
代码详解
这个函数的目的是生成一个累加数组,其中每个元素都是之前元素的总和。该函数接受一个数字类型的数组作为参数,并返回一个数字类型的数组。
首先,函数接受一个数字类型的数组 arr
作为参数。接下来,它使用 reduce
方法来遍历该数组中的所有元素。回调函数接受三个参数:
a
:累加器变量,它存储生成的累加数组。b
:当前元素的值。i
:当前元素在数组中的索引。
在回调函数的主体中,我们检查当前元素是否是数组的第一个元素。如果是,我们将其添加到新数组中。否则,我们将上一个元素的值添加到当前元素的值中,并将结果添加到新数组中。
使用示例
accumulate([1, 2, 3, 4]); // [1, 3, 6, 10]
// 1 = 1
// 1 + 2 = 3
// 1 + 2 + 3 = 6
// 1 + 2 + 3 + 4 = 10
13、查找数组中最后一个匹配元素的索引
可视版
可复制版
const lastIndex = <T,_>(arr: T[], predicate: (a: T) => boolean): number => arr.map((item) => predicate(item)).lastIndexOf(true);
代码详解
该函数接受两个参数:一个类型为 T
的数组 arr
和一个类型为 (a: T) => boolean
的函数 predicate
,用于指定要查找的条件。在函数体内部,它调用了数组的 map
方法,将 predicate
应用于数组中的每个元素,将返回值构成一个新的布尔值数组。然后,它调用了新数组的 lastIndexOf
方法,查找其中值为 true
的最后一个元素的索引,并将其作为函数的返回值
使用示例
lastIndex([1, 3, 5, 7, 9, 2, 4, 6, 8], (i) => i % 2 === 1); // 4
lastIndex([1, 3, 5, 7, 9, 8, 6, 4, 2], (i) => i > 6); // 5
14、给数组中的元素排名
可视版
可复制版
const ranking = (arr: number[]): number[] => arr.map((x, y, z) => z.filter((w) => w > x).length + 1);
代码详解
该函数接受一个数字数组 arr
作为参数,返回一个新的数字数组,其中每个元素表示对应原数组中的元素在数组中的排名。
函数的实现是对原数组中的每个元素 x
应用一个箭头函数,该箭头函数接受三个参数:x
表示当前元素的值,y
表示当前元素在数组中的索引,z
表示原始数组 arr
。然后,该箭头函数使用 filter
方法筛选出所有大于 x
的元素,并返回筛选出的元素个数加上 1,这就是 x
在数组中的排名。
具体来说,filter
方法接受一个函数 w
作为参数,该函数返回一个布尔值,表示该元素是否符合条件。在这个函数中,我们将 w
和 x
进行比较,筛选出所有大于 x
的元素,并返回它们的个数。由于排名是从 1 开始计数的,因此最终结果需要加上 1。
注意,如果数组中存在多个相同的元素,它们在排名中将占据相同的位置,即具有相同的排名。
使用示例
ranking([80, 65, 90, 50]); // [2, 3, 1, 4]
ranking([80, 80, 70, 50]); // [1, 1, 3, 4]
ranking([80, 80, 80, 50]); // [1, 1, 1, 4]
15、移除数组中所有假值
可视版
可复制版
const removeFalsy = <T,_>(arr: T[]): T[] => arr.filter(Boolean);
代码详解
该函数接受一个类型为 T
的数组 arr
,并返回一个新的类型为 T
的数组,其中移除了所有的假值。
函数的实现是使用数组的 filter
方法和全局函数 Boolean
。在 JavaScript 中,假值是指 false
、0
、''
、null
、undefined
和 NaN
。全局函数 Boolean
会将任何值转换为其对应的布尔值,例如,Boolean(0)
的返回值为 false
。
因此,使用 arr.filter(Boolean)
的方式可以筛选出数组中的所有真值(Truthy Value),并返回一个新的数组。这个新数组中包含原数组中所有的真值,而假值则被移除了。
使用示例
removeFalsy([0, 'a string', '', NaN, true, 5, undefined, 'another string', false]);
// ['a string', true, 5, 'another string']
16、合并数组同时去除重复元素
可视版
可复制版
const union = <T,_>(...arr: T[][]): T[] => [...new Set(arr.flat())];
代码详解
该函数接受多个类型为 T
的数组 arr
作为参数,返回一个新的类型为 T
的数组,其中包含了所有输入数组中的元素,且不包含重复的元素。
函数的实现分为两个步骤。首先,使用数组的扩展语法 ...
将所有输入数组展开为一个二维数组。例如,如果函数调用为 union([1, 2], [2, 3], [3, 4])
,则展开后的二维数组为 [[1, 2], [2, 3], [3, 4]]
。
然后,使用数组的 flat
方法将二维数组转换为一维数组,并使用 Set
对象去除其中的重复元素。最终,使用扩展语法将结果转换为一个新的数组,并返回该数组作为函数的结果。
使用示例
union([1, 2], [2, 3], [3]); // [1, 2, 3]
17、将数组转换为特定键的值为键的对象
可视版
可复制版
const toObject = <T extends Record<string, any>, K extends keyof T>(arr: T[], key: K): Record<string, T> => (
Object.fromEntries(arr.map((it) => [it[key], it]))
);
标题有点绕,直接看下面的使用示例吧
代码详解
该函数接受两个参数:一个类型为 T
的数组 arr
,和一个类型为 K
的键 key
,该键是 T
类型的属性之一。函数返回一个新的类型为 Record<string, T>
的对象,其中对象的键为数组中每个元素的 key
属性的值,而对象的值为该元素本身。
函数的实现是使用了泛型约束和 TypeScript 中的类型推导。在该代码中,使用 extends
关键字限制了 T
必须是一个包含任意类型属性的对象,而 K
必须是 T
中的一个属性名。这样可以确保数组中的每个元素都包含 key
属性,并且该属性可以用作对象的键。
然后,使用 map
方法遍历数组中的每个元素 it
,将其转换为一个包含键值对 [it[key], it]
的数组。最后,使用 Object.fromEntries
方法将这个数组转换为一个新的对象,并将其作为函数的返回值。
使用示例
toObject(
[
{ id: '1', name: 'Alpha', gender: 'Male' },
{ id: '2', name: 'Bravo', gender: 'Male' },
{ id: '3', name: 'Charlie', gender: 'Female' },
],
'id'
);
/*
{
'1': { id: '1', name: 'Alpha', gender: 'Male' },
'2': { id: '2', name: 'Bravo', gender: 'Male' },
'3': { id: '3', name: 'Charlie', gender: 'Female' },
}
*/
18、根据对象的某个键值对数组进行排序
可视版
可复制版
const sortBy = <T extends Record<string, any>, K extends keyof T>(arr: T[], k: K): T[] => (
arr.concat().sort((a, b) => (a[k] > b[k] ? 1 : a[k] < b[k] ? -1 : 0))
);
代码详解
该函数接受两个参数:一个类型为 T
的数组 arr
,和一个类型为 K
的键 k
,该键是 T
类型的属性之一。函数返回一个新的类型为 T
的数组,其中数组的元素按照键 k
的值进行排序。
函数的实现使用了泛型约束和 TypeScript 中的类型推导。在该代码中,使用 extends
关键字限制了 T
必须是一个包含任意类型属性的对象,而 K
必须是 T
中的一个属性名。这样可以确保数组中的每个元素都包含 k
属性,并且该属性可以用作排序依据。
然后,使用 concat
方法创建数组的副本,并使用 sort
方法对副本进行排序。在排序函数中,使用三元运算符比较 a[k]
和 b[k]
的值,如果 a[k]
大于 b[k]
,则返回 1,表示 a
应该排在 b
后面;如果 a[k]
小于 b[k]
,则返回 -1,表示 a
应该排在 b
前面;如果 a[k]
等于 b[k]
,则返回 0,表示 a
和 b
的顺序不变。
最后,将排序后的副本数组作为函数的返回值。注意,原始数组 arr
不会被修改,因为使用了 concat
方法创建了一个副本。
使用示例
const people = [
{ name: 'Foo', age: 42 },
{ name: 'Bar', age: 24 },
{ name: 'Fuzz', age: 36 },
{ name: 'Baz', age: 32 },
];
sortBy(people, 'age');
// [
// { name: 'Bar', age: 24 },
// { name: 'Baz', age: 32 },
// { name: 'Fuzz', age: 36 },
// { name: 'Foo', age: 42 },
// ]
结语
这些代码片段加上详细的讲解以及使用示例,是不是瞬间就变得通俗易懂了!
作者:HashTang
转载自:https://juejin.cn/post/7212996764387344441