通过.groupBy()和.groupByToMap()将数组分组的方法

650 阅读3分钟

目录:


本提案引入了两个新的数组方法。

  • 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;
};