目录:
本提案引入了两个新的数组方法。
array.groupBy(callback, thisArg?)array.groupByToMap(callback, thisArg?)
这些是它们的类型签名。
Array<Elem>.prototype.groupBy<GroupKey extends (string|symbol)>(
callback: (value: Elem, index: number, array: Array<Elem>) => GroupKey,
thisArg?: any
): {[k: GroupKey]: Array<Elem>}
Array<Elem>.prototype.groupByToMap<GroupKey>(
callback: (value: Elem, index: number, array: Array<Elem>) => GroupKey,
thisArg?: any
): Map<GroupKey, Array<Elem>>
这两个方法都是对数组进行分组。
- 输入:一个数组
- 输出:组。每个组有一个组键和一个包含组成员的数组。
该算法在数组上进行迭代。对于每个数组元素,它向其回调函数询问一个组键,并将该元素添加到相应的组。
因此。如果我们忽略元素的顺序,将所有组成员串联起来就等于输入数组。
这两个方法的不同之处在于它们如何表示组。
.groupBy()将组存储在一个对象中。组的键被存储为属性键。组的成员被存储为属性值。.groupByToMap()将群组存储在一个Map中。组的键被存储为地图的键。组成员被存储为地图值。
在下一节中,我们将研究分组的用例,以及在哪个用例中使用哪种方法。
这是分组的第一个例子。
const groupBySign = (nums) => nums.groupBy(
(elem) => {
if (elem < 0) {
return 'negative';
} else if (elem === 0) {
return 'zero';
} else {
return 'positive';
}
}
);
assert.deepEqual(
groupBySign([0, -5, 3, -4, 8, 9]),
{
__proto__: null,
zero: [0],
negative: [-5, -4],
positive: [3, 8, 9],
}
);
由.groupBy() 返回的对象的原型是null 。这使得它成为一个更好的字典,因为没有属性被继承,而且属性__proto__ 没有任何特殊的行为(详见"JavaScript for impatient programmers")。
如何在.groupBy() 和.groupByToMap() 之间进行选择?
我们如何在这两种分组方法中选择?
如果我们想取消结构(并且我们提前知道分组的键),我们使用.groupBy() 。
const {negative, positive} = groupBySign([0, -5, 3, -4, 8, 9]);
否则,使用Map的好处是键值不限于字符串和符号。我们将很快看到一个关于.groupByToMap() 的例子。
分组的用例
这是分组数组的三个常见用例。
- 处理案例。
- 有一组固定的组键,我们提前知道。
- 我们希望每个案例都有一个数组的值。
- 按属性值分组。
- 我们得到一个任意的组键集。
- 我们对[组键,组成员]对感兴趣。
- 计算组的成员。
- 这个用例类似于按属性值分组,但我们只对多少个输入数组元素有一个给定的属性值感兴趣,而不是对它们是哪些元素感兴趣。
接下来,我们将看到每个用例的例子,以及两种分组方法中哪一种更适合。
处理案例
Promise组合器Promise.allSettled()返回数组,比如下面这个。
const settled = [
{ status: 'rejected', reason: 'Jhon' },
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
];
我们可以对数组元素进行如下分组。
const {fulfilled, rejected} = settled.groupBy(x => x.status); // (A)
// Handle fulfilled results
assert.deepEqual(
fulfilled,
[
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
]
);
// Handle rejected results
assert.deepEqual(
rejected,
[
{ status: 'rejected', reason: 'Jhon' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
]
);
对于这个用例,.groupBy() ,效果更好,因为我们可以使用析构(A行)。
按属性值分组
在下一个例子中,我们想按国家对人员进行分组。
const persons = [ { name: 'Louise', country: 'France' }, { name: 'Felix', country: 'Germany' }, { name: 'Ava', country: 'USA' }, { name: 'Léo', country: 'France' }, { name: 'Oliver', country: 'USA' }, { name: 'Leni', country: 'Germany' },];
assert.deepEqual(
persons.groupByToMap((person) => person.country),
new Map([
[
'France',
[
{ name: 'Louise', country: 'France' },
{ name: 'Léo', country: 'France' },
]
],
[
'Germany',
[
{ name: 'Felix', country: 'Germany' },
{ name: 'Leni', country: 'Germany' },
]
],
[
'USA',
[
{ name: 'Ava', country: 'USA' },
{ name: 'Oliver', country: 'USA' },
]
],
])
);
对于这个用例,.groupByToMap() 是一个更好的选择,因为我们可以在Maps中使用任意的键,而在对象中,键被限制为字符串和符号。
计算元素
在下面的例子中,我们计算每个词在一个给定文本中出现的频率。
function countWords(text) {
const words = text.split(' ');
const groupMap = words.groupByToMap((word) => word);
return new Map(
Array.from(groupMap) // (A)
.map(([word, wordArray]) => [word, wordArray.length])
);
}
assert.deepEqual(
countWords('knock knock chop chop buffalo buffalo buffalo'),
new Map([
['buffalo', 3],
['chop', 2],
['knock', 2],
])
);
我们只对组的大小感兴趣。在A行,我们通过一个数组绕道而行,因为地图没有一个方法.map() 。
再一次,我们使用.groupByToMap() ,因为Maps可以有任意的键。
如果每个输入的Array元素可以属于多个组呢?
如果输入数组的每个元素都可以属于多个组,我们就不能使用分组方法。那么我们就必须编写我们自己的分组函数--比如说。
function multiGroupByToMap(arr, callback, thisArg) {
const result = new Map();
for (const [index, elem] of arr.entries()) {
const groupKeys = callback.call(thisArg, elem, index, this);
for (const groupKey of groupKeys) {
let group = result.get(groupKey);
if (group === undefined) {
group = [];
result.set(groupKey, group);
}
group.push(elem);
}
}
return result;
}
function groupByTag(objs) {
return multiGroupByToMap(objs, (obj) => obj.tags);
}
const articles = [
{title: 'Sync iteration', tags: ['js', 'iter']},
{title: 'Promises', tags: ['js', 'async']},
{title: 'Async iteration', tags: ['js', 'async', 'iter']},
];
assert.deepEqual(
groupByTag(articles),
new Map([
['js', [
{title: 'Sync iteration', tags: ['js', 'iter']},
{title: 'Promises', tags: ['js', 'async']},
{title: 'Async iteration', tags: ['js', 'async', 'iter']},
]
],
['iter', [
{title: 'Sync iteration', tags: ['js', 'iter']},
{title: 'Async iteration', tags: ['js', 'async', 'iter']},
]
],
['async', [
{title: 'Promises', tags: ['js', 'async']},
{title: 'Async iteration', tags: ['js', 'async', 'iter']},
]
],
])
);
实现
自己实现分组
这些是两种分组方法的简单实现。
Array.prototype.groupBy = function (callback, thisArg) {
const result = Object.create(null);
for (const [index, elem] of this.entries()) {
const groupKey = callback.call(thisArg, elem, index, this);
if (! (groupKey in result)) {
result[groupKey] = [];
}
result[groupKey].push(elem);
}
return result;
};
Array.prototype.groupByToMap = function (callback, thisArg) {
const result = new Map();
for (const [index, elem] of this.entries()) {
const groupKey = callback.call(thisArg, elem, index, this);
let group = result.get(groupKey);
if (group === undefined) {
group = [];
result.set(groupKey, group);
}
group.push(elem);
}
return result;
};
库
- core-js有
.groupBy()和.groupByToMap()的poyfills。 - Lodash方法
_.groupBy(),等同于array.groupBy()。