GrouBy
用过 Lodash.groupBy 的童鞋肯定见识到这个工具函数是多么的美妙;官方对该函数的描述是这样的
Creates an object composed of keys generated from the results of running each element of
collection
thruiteratee
. The order of grouped values is determined by the order they occur incollection
. 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
版本已经包含了该函数(Object
和 Map
)。
说到这里有人问了,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会如何去写?
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);
运行结果如下
或者你的个人实力比较强,你可以选择使用数组的reduce方法
js
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,我们只需要一行代码即可解决:
js
const peopleByAge = Object.groupBy(people, (person) => person.age);
此外有趣的是,Object.groupBy
返回的是一个空原型对象,这意味着返回值不会继承自 Object.prototype
的任何属性,当然这也意味着我们无法通过这个返回值来调用原本Object对象的所有方法,比如 hasOwnProperty
或 toString
等方法。
Callback参数
Object.groupBy
方法的第二次参数(回调函数)应该返回一个字符串或一个符号(Symbol)。如果它返回其他类型的值,那就会被强制转换为字符串。
例子
在这个例子中,年龄属性是number类型的数据,但在结果中它被强制转换为字符串。虽然但是,我们依然可以使用数字作为属性访问,因为使用方括号表示法会将参数强制转换为字符串来处理。
Map.groupBy
Map.groupBy
做的事情和 Object.groupBy
很相近,只是它返回的结果是个Map
对象,这就意味着我们可以在返回值的基础上调用所有Map对象的方法,这一点看,Map.groupBy
显得更加亲和,不那么霸道。
Callback参数
Object.groupBy
方法的第二次参数(回调函数)可以返回任何类型的值。
举个栗子
js
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是必须是相同的,即 ===
。
js
peopleByManager.get(ceo);
// => [{ name: "Alice", age: 28, reportsTo: ceo }, { name: "Eve", age: 28, reportsTo: ceo }]
peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined
写到这里,原生的groupBy方法基本介绍完毕,是不是觉得很酷?
为什么是Object和Map的静态方法
也许你们会在想为什么我们把这个功能叫做 Object.groupBy
而不是 Array.prototype.groupBy
?原因在于曾经有一个库在 Array.prototype
上添加了一个名为 groupBy
的方法,但这个方法并没有考虑到兼容问题。所以当我们考虑为网络引入新的功能时,与以前的代码兼容是非常重要的,因为许多网站和应用程序依赖于旧的代码。这个问题在几年前曾引起很多人的关注,当时尝试引入 Array.prototype.flatten
方法时发生了一场狗血的事件,被戏称为 "SmooshGate"。这就是为什么我们选择了 Object.groupBy
而不是 Array.prototype.groupBy
,为的就是避免潜在的兼容性问题。
写在最后
很高兴看到JavaScript一直在弥补自身的一些不足,我也相信在未来也会持续不断得将常用的工具函数收纳并转正。咱前端开发环境也是未来可期呀!!!