17

JavaScript 实现更多数组的高阶函数 - rxliuli blog

 3 years ago
source link: https://blog.rxliuli.com/p/41ccac34786d40e6acc313e95fe0e9c8/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

为什么有了 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
20
21
/**
* 比较两个数组的差异
* @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 对函数式编程支持很好,所以习惯高阶函数于我们而言是一件好事,将问题的本质抽离出来,而不是每次都局限于某个具体的问题上。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK