头图

GrouBy

用过 Lodash.groupBy 的童鞋肯定见识到这个工具函数是多么的美妙;官方对该函数的描述是这样的

Creates an object composed of keys generated from the results of running each element of collection thru iteratee. The order of grouped values is determined by the order they occur in collection. The corresponding value of each key is an array of elements responsible for generating the key. The iteratee is invoked with one argument:  (value) .

译文

创建一个对象,其中的键是通过将collection中的每个元素通过iteratee运行的结果生成的。分组值的顺序由它们在collection中出现的顺序确定。每个键对应的值是生成该键的元素数组。iteratee使用一个参数进行调用:(value)

其实简而言之就是根据一个规则将数组中的元素进行归类并返回。这个场景遍布于Web开发的每一个角落,很多涉及数据转换的场景,可能或多或少与之相关。然而在此之前我们想使用到这个方法,只能安装第三方工具包(如Lodash、UnderScore等)或者垫片来实现。直到最近,原生groupBy方法终于得到了浏览器的支持。

提案

原生groupBy 作为 TC39提案 的一部分,目前处于 Stage-3 阶段,且目前Chrome v117 版本已经包含了该函数(ObjectMap)。

说到这里有人问了,Stage-3表示什么意思?下面是对每一个Stage所代表的含义简单描述下
TC39提案的stage分别代表以下意思:

  • Stage 0 - Strawman (草案阶段): 这是提案的初始阶段,通常是一些初步的想法或建议。这些提案还没有得到正式的讨论和接受。
  • Stage 1 - Proposal (提案阶段): 在这个阶段,提案已经经过了初步的讨论,并且有了详细的说明。它们通常由一个或多个TC39委员会成员提交,并等待进一步的审查和反馈。
  • Stage 2 - Draft (草案阶段): 在这个阶段,提案已经经过了初步的审查,包括语法和语义方面的考虑。提案可能会在这个阶段进行一些修改和改进。
  • Stage 3 - Candidate (候选阶段): 当提案达到这个阶段时,它们被认为是成熟的,可以被实施到JavaScript引擎中。这通常包括详细的规范文档和实际的参考实现。
  • Stage 4 - Finished (完成阶段): 这是提案的最终阶段,表示它们已经被正式接受为ECMAScript标准的一部分,可以在各种JavaScript环境中广泛使用。

原生GroupBy

前面我们提到,原生的groupBy方法分两种,即Object.groupBy 和 Map.groupBy。下面通过一个例子让我们来了解一下它们的实力到底如何

假如我们需要对如下的数组数据按年龄进行分组,那么在不使用groupBy方法情况下用原生js会如何去写?

const people = [
  { name: "Alice", age: 28 },
  { name: "Bob", age: 30 },
  { name: "Eve", age: 28 },
];

const peopleByAge = {};

people.forEach((person) => {
  const age = person.age;
  if (!peopleByAge[age]) {
    peopleByAge[age] = [];
  }
  peopleByAge[age].push(person);
});

console.log(peopleByAge);

运行结果如下

image.png

或者你的个人实力比较强,你可以选择使用数组的reduce方法

const peopleByAge = people.reduce((acc, person) => {
  const age = person.age;
  if (!acc[age]) {
    acc[age] = [];
  }
  acc[age].push(person);
  return acc;
}, {});

依然可以实现我们的原始需求。但是无论上述哪种方式,代码都显得很“啰嗦”,为什么呢?因为我们总要去一些检查,比如:if (!acc[age]),不然代码就会Crash。

Object.groupBy

用Object.groupBy,我们只需要一行代码即可解决:

const peopleByAge = Object.groupBy(people, (person) => person.age);

此外有趣的是,Object.groupBy 返回的是一个空原型对象,这意味着返回值不会继承自 Object.prototype的任何属性,当然这也意味着我们无法通过这个返回值来调用原本Object对象的所有方法,比如 hasOwnPropertytoString 等方法。

Callback参数

Object.groupBy 方法的第二次参数(回调函数)应该返回一个字符串或一个符号(Symbol)。如果它返回其他类型的值,那就会被强制转换为字符串。

例子

在这个例子中,年龄属性是number类型的数据,但在结果中它被强制转换为字符串。虽然但是,我们依然可以使用数字作为属性访问,因为使用方括号表示法会将参数强制转换为字符串来处理。

Map.groupBy

Map.groupBy 做的事情和 Object.groupBy 很相近,只是它返回的结果是个Map对象,这就意味着我们可以在返回值的基础上调用所有Map对象的方法,这一点看,Map.groupBy 显得更加亲和,不那么霸道。

Callback参数

Object.groupBy 方法的第二次参数(回调函数)可以返回任何类型的值。

举个栗子

const ceo = { name: "Jamie", age: 40, reportsTo: null };
const manager = { name: "Alice", age: 28, reportsTo: ceo };

const people = [
  ceo
  manager,
  { name: "Bob", age: 30, reportsTo: manager },
  { name: "Eve", age: 28, reportsTo: ceo },
];

const peopleByManager = Map.groupBy(people, (person) => person.reportsTo);

这个例子中,我们是根据员工的汇报对象来进行分组。需要注意的是,如果要从返回值获取数据,那么get参数必须与Map的key是必须是相同的,即 ===

peopleByManager.get(ceo);
// => [{ name: "Alice", age: 28, reportsTo: ceo }, { name: "Eve", age: 28, reportsTo: ceo }]
peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined

image.png

写到这里,原生的groupBy方法基本介绍完毕,是不是觉得很酷?

为什么是Object和Map的静态方法

也许你们会在想为什么我们把这个功能叫做 Object.groupBy 而不是 Array.prototype.groupBy?原因在于曾经有一个库在 Array.prototype 上添加了一个名为 groupBy 的方法,但这个方法并没有考虑到兼容问题。所以当我们考虑为网络引入新的功能时,与以前的代码兼容是非常重要的,因为许多网站和应用程序依赖于旧的代码。这个问题在几年前曾引起很多人的关注,当时尝试引入 Array.prototype.flatten 方法时发生了一场狗血的事件,被戏称为 "SmooshGate"。这就是为什么我们选择了 Object.groupBy 而不是 Array.prototype.groupBy,为的就是避免潜在的兼容性问题。

写在最后

很高兴看到JavaScript一直在弥补自身的一些不足,我也相信在未来也会持续不断得将常用的工具函数收纳并转正。咱前端开发环境也是未来可期呀!!!


风吹过的夏夜
295 声望31 粉丝

前端工程师