JavaScript 实现更多数组的高阶函数

本文最后更新于:2020年12月30日 凌晨

场景

虽说人人平等,但有些人更加平等。

为什么有了 Lodash 这种通用函数工具库,吾辈要写这篇文章呢?吾辈在 SegmentFault 上经常看到关于 JavaScript 数组的相关疑问,甚至于,相同类型的问题,只是数据变化了一些,就直接提出了一个新的问题(实际上,对自身并无帮助)。简单搜索了一下 Array,居然有 2360+ 条的结果,足可见这类问题的频率之高。若是有一篇适合 JavaScript 萌新阅读的自己实现数组更多操作的文章,情况或许会发生一些变化。

下面吾辈便来实现以下几种常见的操作

  • uniqueBy: 去重
  • sortBy: 排序
  • filterItems: 过滤掉一些元素
  • diffBy: 差异
  • groupBy: 分组
  • arrayToMap: Array 转换为 Map
  • 递归操作

前言:
你至少需要了解 ES6 的一些特性你才能愉快的阅读

uniqueBy: 去重

相关问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* js 的数组去重方法
* @param arr 要进行去重的数组
* @param kFn 唯一标识元素的方法,默认使用 {@link returnItself}
* @returns 进行去重操作之后得到的新的数组 (原数组并未改变)
*/
function uniqueBy(arr, kFn = (val) => val) {
const set = new Set()
return arr.filter((v, ...args) => {
const k = kFn(v, ...args)
if (set.has(k)) {
return false
}
set.add(k)
return true
})
}

使用

1
2
console.log(uniqueBy([1, 2, 3, '1', '2'])) // [ 1, 2, 3, '1', '2' ]
console.log(uniqueBy([1, 2, 3, '1', '2'], (i) => i + '')) // [ 1, 2, 3 ]

sortBy: 排序

相关问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 快速根据指定函数对数组进行排序
* 注: 使用递归实现,对于超大数组(其实前端的数组不可能特别大吧?#笑)可能造成堆栈溢出
* @param arr 需要排序的数组
* @param kFn 对数组中每个元素都产生可比较的值的函数,默认返回自身进行比较
* @returns 排序后的新数组
*/
function sortBy(arr, kFn = (v) => v) {
// TODO 此处为了让 typedoc 能生成文档而不得不加上类型
const newArr = arr.map((v, i) => [v, i])
function _sort(arr, fn) {
// 边界条件,如果传入数组的值
if (arr.length <= 1) {
return arr
}
// 根据中间值对数组分治为两个数组
const medianIndex = Math.floor(arr.length / 2)
const medianValue = arr[medianIndex]
const left = []
const right = []
for (let i = 0, len = arr.length; i < len; i++) {
if (i === medianIndex) {
continue
}
const v = arr[i]
if (fn(v, medianValue) <= 0) {
left.push(v)
} else {
right.push(v)
}
}
return _sort(left, fn).concat([medianValue]).concat(_sort(right, fn))
}
return _sort(newArr, ([t1, i1], [t2, i2]) => {
const k1 = kFn(t1, i1, arr)
const k2 = kFn(t2, i2, arr)
if (k1 === k2) {
return 0
} else if (k1 < k2) {
return -1
} else {
return 1
}
}).map(([_v, i]) => arr[i])
}

使用

1
2
3
console.log(sortBy([1, 3, 5, 2, 4])) // [ 1, 2, 3, 4, 5 ]
console.log(sortBy([1, 3, 5, '2', '4'])) // [ 1, '2', 3, '4', 5 ]
console.log(sortBy([1, 3, 5, '2', '4'], (i) => -i)) // [ 5, '4', 3, '2', 1 ]

filterItems: 过滤掉一些元素

相关问题

1
2
3
4
5
6
7
8
9
10
11
/**
* 从数组中移除指定的元素
* 注: 时间复杂度为 1~3On
* @param arr 需要被过滤的数组
* @param deleteItems 要过滤的元素数组
* @param kFn 每个元素的唯一键函数
*/
function filterItems(arr, deleteItems, kFn = (v) => v) {
const kSet = new Set(deleteItems.map(kFn))
return arr.filter((v, i, arr) => !kSet.has(kFn(v, i, arr)))
}

使用

1
2
console.log(filterItems([1, 2, 3, 4, 5], [1, 2, 0])) // [ 3, 4, 5 ]
console.log(filterItems([1, 2, 3, 4, 5], ['1', '2'], (i) => i + '')) // [ 3, 4, 5 ]

diffBy: 差异

相关问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 比较两个数组的差异
* @param left 第一个数组
* @param right 第二个数组
* @param kFn 每个元素的唯一标识产生函数
* @returns 比较的差异结果
*/
function diffBy(left, right, kFn = (v) => v) {
// 首先得到两个 kSet 集合用于过滤
const kThanSet = new Set(left.map(kFn))
const kThatSet = new Set(right.map(kFn))
const leftUnique = left.filter((v, ...args) => !kThatSet.has(kFn(v, ...args)))
const rightUnique = right.filter(
(v, ...args) => !kThanSet.has(kFn(v, ...args)),
)
const kLeftSet = new Set(leftUnique.map(kFn))
const common = left.filter((v, ...args) => !kLeftSet.has(kFn(v, ...args)))
return { left: leftUnique, right: rightUnique, common }
}

使用

1
2
3
console.log(diffBy([1, 2, 3], [2, 3, 4])) // { left: [ 1 ], right: [ 4 ], common: [ 2, 3 ] }
console.log(diffBy([1, 2, 3], ['2', 3, 4])) // { left: [ 1, 2 ], right: [ '2', 4 ], common: [ 3 ] }
console.log(diffBy([1, 2, 3], ['2', 3, 4], (i) => i + '')) // { left: [ 1 ], right: [ 4 ], common: [ 2, 3 ] }

groupBy: 分组

相关问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* js 数组按照某个条件进行分组
*
* @param arr 要进行分组的数组
* @param kFn 元素分组的唯一标识函数
* @param vFn 元素分组的值处理的函数。第一个参数是累计值,第二个参数是当前正在迭代的元素,如果你使用过 {@link Array#reduce} 函数的话应该对此很熟悉
* @param init 每个分组的产生初始值的函数。类似于 reduce 的初始值,但它是一个函数,避免初始值在所有分组中进行累加。
* @returns 元素标识 => 数组映射 Map
*/
function groupBy(
arr,
kFn = (v) => v,
/**
* 默认的值处理函数
* @param res 最终 V 集合
* @param item 当前迭代的元素
* @returns 将当前元素合并后的最终 V 集合
*/
vFn = (res, item) => {
res.push(item)
return res
},
init = () => [],
) {
// 将元素按照分组条件进行分组得到一个 条件 -> 数组 的对象
return arr.reduce((res, item, index, arr) => {
const k = kFn(item, index, arr)
// 如果已经有这个键了就直接追加, 否则先将之初始化再追加元素
if (!res.has(k)) {
res.set(k, init())
}
res.set(k, vFn(res.get(k), item, index, arr))
return res
}, new Map())
}

使用

1
2
3
4
5
6
7
8
9
10
console.log(groupBy([1, 2, 2, 2, 4, 4, 5, 5, 6], (i) => i)) // Map { 1 => [ 1 ],  2 => [ 2, 2, 2 ],  4 => [ 4, 4 ],  5 => [ 5, 5 ],  6 => [ 6 ] }
console.log(groupBy([1, 2, 2, 2, 4, 4, 5, 5, 6], (i) => i % 2 === 0)) // Map { false => [ 1, 5, 5 ], true => [ 2, 2, 2, 4, 4, 6 ] }
console.log(
groupBy(
[1, 2, 2, 2, 4, 4, 5, 5, 6],
(i) => i % 2 === 0,
(res, i) => res.add(i),
() => new Set(),
),
) // Map { false => Set { 1, 5 }, true => Set { 2, 4, 6 } }

arrayToMap: 转换为 Map

相关问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 将数组映射为 Map
* @param arr 数组
* @param k 产生 Map 元素唯一标识的函数,或者对象元素中的一个属性名
* @param v 产生 Map 值的函数,默认为返回数组的元素,或者对象元素中的一个属性名
* @returns 映射产生的 map 集合
*/
export function arrayToMap(arr, k, v = (val) => val) {
const kFn = k instanceof Function ? k : (item) => Reflect.get(item, k)
const vFn = v instanceof Function ? v : (item) => Reflect.get(item, v)
return arr.reduce(
(res, item, index, arr) =>
res.set(kFn(item, index, arr), vFn(item, index, arr)),
new Map(),
)
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const county_list = [
{
id: 1,
code: '110101',
name: '东城区',
citycode: '110100',
},
{
id: 2,
code: '110102',
name: '西城区',
citycode: '110100',
},
{
id: 3,
code: '110103',
name: '崇文区',
citycode: '110100',
},
]
console.log(arrayToMap(county_list, 'code', 'name')) // Map { '110101' => '东城区', '110102' => '西城区', '110103' => '崇文区' }
console.log(
arrayToMap(
county_list,
({ code }) => code,
({ name }) => name,
),
) // Map { '110101' => '东城区', '110102' => '西城区', '110103' => '崇文区' }

递归

相关问题

以上种种操作皆是对一层数组进行操作,如果我们想对嵌套数组进行操作呢?例如上面这两个问题?其实问题是类似的,只是递归遍历数组而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* js 的数组递归去重方法
* @param arr 要进行去重的数组
* @param kFn 唯一标识元素的方法,默认使用 {@link returnItself},只对非数组元素生效
* @returns 进行去重操作之后得到的新的数组 (原数组并未改变)
*/
function deepUniqueBy(arr, kFn = (val) => val) {
const set = new Set()
return arr.reduce((res, v, i, arr) => {
if (Array.isArray(v)) {
res.push(deepUniqueBy(v))
return res
}
const k = kFn(v, i, arr)
if (!set.has(k)) {
set.add(k)
res.push(v)
}
return res
}, [])
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
const testArr = [
1,
1,
3,
'hello',
[3, 4, 4, 'hello', '5', [5, 5, ['a', 'r']]],
{
key: 'test',
},
4,
[3, 0, 2, 3],
]
console.log(deepUniqueBy(testArr)) // [ 1,  3,  'hello',  [ 3, 4, 'hello', '5', [ 5, [Object] ] ],  { key: 'test' },  4,  [ 3, 0, 2 ] ]

反例

事实上,目前 SegmentFault 上存在着大量低质量且重复的问题及回答,关于这点确实比不上 StackOverflow。下面是两个例子,可以看一下能否发现什么问题

事实上,不管是问题还是答案,都没有突出核心 – Array 映射为 Map/Array 分组,而且这种问题和答案还层出不穷。如果对 Array 的 API 都没有看过一遍就来询问的话,对于帮助者来说却是太失礼了!

总结

JavaScript 对函数式编程支持很好,所以习惯高阶函数于我们而言是一件好事,将问题的本质抽离出来,而不是每次都局限于某个具体的问题上。


JavaScript 实现更多数组的高阶函数
https://blog.rxliuli.com/p/875aa4331b56435aac71d3b654c27950/
作者
rxliuli
发布于
2020年2月2日
许可协议