在日常的开发实践中,数据分组是一个很常用的操作。一般情况下,我们需要编写自定义的分组函数或者借助于三方库中的 groupBy
函数来实现。不过,有个激动人心的好消息:谷歌在117
版本,ECMAScript
更新了两个原生的分组方法——Object.groupBy
和 Map.groupBy
。
让我们来回忆一下,在以往的开发过程中,我们是怎么自定义数组分组方法的。假设我们有一个由员工对象组成的数组,并且我们希望按照年龄来对这些员工进行分组。以往,我们可能会使用 forEach
循环来达到目的,相关代码如下所示:
const members = [
{ name: "HuaXiaoWen", age: 23 },
{ name: "HuaXiaoMing", age: 25 },
{ name: "HuaXiaoGang", age: 23 },
];
const membersGroupByAge = {};
members.forEach((member) => {
const age = member.age;
if (!membersGroupByAge[age]) {
membersGroupByAge[age] = [];
}
membersGroupByAge[age].push(member);
});
console.log(membersGroupByAge);
执行上述代码,我们会得到以下的输出结果:
{
"23": [
{
"name": "HuaXiaoWen",
"age": 23
},
{
"name": "HuaXiaoGang",
"age": 23
}
],
"25": [
{
"name": "HuaXiaoMing",
"age": 25
}
]
}
此外,我们也可以选择使用 reduce
方法来完成同样的任务:
const membersGroupByAge = members.reduce((accumulator, member) => {
const age = member.age;
if (!accumulator[age]) {
accumulator[age] = [];
}
accumulator[age].push(member);
return accumulator;
}, {});
显而易见的,无论是哪一种方法,代码都显得有些冗余且不够简洁。每当分组时,我们都需要检查分组键的值是否出现过,如果没出现过,则需要初始化一个空数组,并将符合条件的元素逐一添加进去。在这种实现下,代码的语义就会略显冗余。
Object.groupBy
Object.groupBy
方法,提供了一个直观的数组分组手段,不仅简化了开发者编写代码的过程,而且也使代码具有更好的可读性。利用这一新方法,我们可以非常方便地按照指定的属性对数组中的对象进行分组。
const membersGroupByAge = Object.groupBy(members, (member) => member.age);
通过上示代码,我们就可以轻松实现数据分组操作。
然而,值得我们注意的是,使用 Object.groupBy
方法返回的对象是一个“纯净”的对象——即没有继承任何原型链上的属性或方法。这一点非常关键,因为它意味着常见的 Object.prototype
方法,如 hasOwnProperty
或 toString
,不会在这个对象上直接可用。虽然这样做有其好处,比如防止了属性名冲突的可能性,但同时也意味着我们在处理返回的分组对象时不能直接使用这些内置方法。例如:
const membersGroupByAge = Object.groupBy(members, (member) => member.age);
// 下面的代码会抛出错误,因为 `membersGroupByAge` 没有 `hasOwnProperty` 方法
console.log(membersGroupByAge.hasOwnProperty("28"));
// TypeError: membersGroupByAge.hasOwnProperty is not a function
// 即使用扩展运算符或者Object.assign来拷贝对象,新的对象仍然不会带有Object的方法
console.log({ ...membersGroupByAge }.hasOwnProperty("28"));
// TypeError: membersGroupByAge.hasOwnProperty is not a function
使用 Object.groupBy
时,需要注意回调函数应该返回 string
或 Symbol
类型的值,因为对象的 key
始终是 string
或 Symbol
。如果回调函数返回其他类型的值(例如数字),key
值也会自动转换为字符串。在我们的例子中,即使 age
属性的值为数字,也会被转换为字符串类型,所以在访问该属性时需要注意这一点。
console.log(membersGroupByAge[23]); // 这是错误的访问方式,因为数字类型的键会被转换为字符串
// => undefined
console.log(membersGroupByAge["23"]); // 这才是正确的访问方式
// => [{"name":"HuaXiaoWen","age":23}, {"name":"HuaXiaoGang","age":23}]
Map.groupBy
Map.groupBy
方法,作为 ECMAScript
新特性的一员,与 Object.groupBy
承担着相似的职责,但它们返回的结果类型有所不同。Map.groupBy
方法的引入为开发者提供了更为灵活的数据结构——Map
对象。与返回普通对象的 Object.groupBy
相比,Map
对象具有一些独特的优势,特别是在进行键的比较时。
以如下代码为例,我们可以看到 Map.groupBy
方法如何根据个体的直接上级(汇报对象)来对人员进行分组:
const manager = { name: "HuaXiaoWen", age: 23, leader: null };
const develop = { name: "HuaXiaoMin", age: 25, leader: "manager" };
const member = [
manager,
develop,
{ name: "HuaXiaoGang", age: 25, leader: "manager" },
{ name: "HuaXiaoLiang", age: 23, leader: null }
];
const memberByLeader = Map.groupBy(members, (member) => member.leader);
在此示例中,我们依据每个人的领队来进行分组,得到的是一个 Map
对象,它以领队作为键,对应的直接下属列表作为值。但要注意,由于 Map
在进行键的比较时使用的是严格等价性检查(===
),因此只有当两个对象的引用完全相同时,它们才被认为是等同的键。
memberByLeader.get(manager);
// => [{ name: "HuaXiaoWen", age: 23, leader: "manager" }, { name: "HuaXiaoGang", age: 23, leader: "manager" }]
memberByLeader.get({ name: "HuaXiaoWen", age: 23, leader: null });
// => undefined
在近期的 JavaScript
发展趋势中,groupBy
方法作为 proposal-array-grouping
提案的一部分,已经引起了广泛关注。该提案目前正处在 ECMAScript
标准化进程的第三阶段,按照目前的进展,我们可以期待在2024
年它能够正式纳入 ECMAScript
标准。
截至2023年12月4日
,Google Chrome
浏览器的117
版本已经率先支持了这一新特性,开发者可以在最新版的 Chrome
中体验到 Object.groupBy
和 Map.groupBy
方法。同时,Mozilla Firefox
的 Nightly
版本已在实验性质的标志后提供了这两个方法的实现。此外, Safari
浏览器也在其平台上以不同的命名实现了相似的功能。
这些方法在 Chrome
中的支持,意味着它们已经在 Google
的 V8
引擎中得到了实现。V8
引擎不仅是 Chrome
和 Chromium
浏览器的驱动核心,也是 Node.js
的基石。因此,随着 V8
引擎的下一次更新,预计这些实用的 groupBy
方法也将很快在 Node.js
环境中可用。
Object
的静态方法而不是 Array
?你可能会好奇为什么选择用 Object.groupBy
而不是像 Array.prototype.groupBy
这样的数组原型方法。
这是因为根据这个提案的说明,曾经有一个库尝试在 Array.prototype
上添加了一个不兼容的 groupBy
方法的补丁。出于向后兼容性的考虑,才将方法放在 Object.prototype
上。
JavaScript
向后兼容性是指在新版本的 JavaScript
中,旧版本的 JavaScript
代码仍然能够正常运行。这是非常重要的,因为很多网站和应用程序使用旧版本的 JavaScript
代码,在更新浏览器或者 JavaScript
引擎之后,这些代码可能会出现错误或者不兼容的情况。
语言规范的兼容性:新版本的
JavaScript
应该能够解析和执行旧版本的JavaScript
代码,同时也应该保留旧版本的语言特性和语法规则。
API
的兼容性:新版本的JavaScript
应该保留旧版本的API
,使得旧版本的JavaScript
应用程序能够正常运行,并且能够使用新版本的API
。浏览器兼容性:新版本的
JavaScript
应该能够在各种浏览器中正常运行,同时也应该保留旧版本的浏览器兼容性,使得旧版本的JavaScript
应用程序能够在不同的浏览器中正常运行。
类似的问题在之前尝试为 Array.prototype
添加 flatten
方法时也发生过,这导致了著名的SmooshGate
争议。
简言之,通过采用静态方法,JavaScript
能够更加稳健地保持其向后兼容性,并且为将来可能引入的新功能和数据结构提供了更为灵活的扩展可能性。
本人每篇文章都是一字一句码出来,希望对大家有所帮助,多提提意见。顺手来个三连击,点赞收藏关注✨,一起加油☕