Object.groupBy 是 JavaScript 语言的最新功能之一,可以根据特定键对数据进行分组。
但这到底意味着什么呢?让我们通过探讨一个实际的使用场景来深入了解。
搜索用户
假设我们有一个从数据库中检索的用户数据集合:
代码语言:javascript复制const users = [
{
id: 1,
email: "first@domain.com",
language: "HTML"
},
{
id: 2,
email: "second@domain.com",
language: "HTML"
},
{
id: 3,
email: "third@domain.com",
language: "CSS"
}
];
要搜索特定用户,传统方法是遍历数组并将每个用户的电子邮件与目标电子邮件进行比较:
代码语言:javascript复制const emailToSearch = "third@domain.com";
let foundUsers = [];
for (const user of users) {
if (user.email === emailToSearch) {
foundUsers.push(user);
}
}
console.log(foundUsers);
// [{ id: 3, email: 'third@domain.com', language: 'CSS' }]
这段代码首先定义了包含要搜索的用户电子邮件的变量。随后,它遍历数组中的每个用户,注意到列表可能是数据库结果,并非所有用户都可能存在。
在每次迭代期间,它检查当前用户的电子邮件是否与指定的搜索电子邮件匹配。如果找到匹配项,则将用户推送到预定义的变量中。此变量被初始化为空数组,以处理用户不匹配搜索的情况。
最后,显示找到的用户。虽然这种方法有效,但 JavaScript 的 Object.groupBy 可以提供更简洁、高效的解决方案。
但是问题是
我们不确定用户是否存在。这很严重,因为每次我们尝试验证用户是否对应于特定电子邮件时,都必须遍历我们数据库中的每个用户。
现在,考虑一个拥有十亿行数据的场景。这个操作将以线性时间的复杂度进行。
虽然不是太糟糕,但还有改进的空间。
索引
您可能会想,为什么我们不使用 SQL 数据库来处理这个问题?如果您有此想法,那太棒了!那就是正确答案。
但不完全是,因为数据库不是一个智能生物,无法提前知道我们的所有问题并为我们优化事物(尽管这是一个值得探讨的有趣想法)。
幸运的是,数据库通过使用索引提供了一种快速处理此类操作的方法。
索引涉及在列上放置特殊标识,并告知我们的数据库,下次当我们需要对该列进行搜索时,请快速处理!
但是,“快速处理”是什么意思呢?简单来说,这意味着根据特定列对所有数据进行分组。这听起来熟悉吗?应该是的,因为这就是使用 Object.groupBy 的目的。
当您在数据库中对列进行索引时,您这样做是因为您预期会返回并用一个请求搜索该列,您需要尽可能快地访问它,最理想的情况是使您的请求花费恒定的时间。
这也是使用 Object.groupBy 时的目标。您的目标是更快地访问数据,因为线性时间不够(例如),您需要更快的访问时间,最理想的情况是恒定时间。
那么改如何运作呢?
首先,您将确定需要快速访问的列。在我们的情况下,这是我们对象的电子邮件列。
其次,您需要创建此特殊索引对象(或分组对象)。
代码语言:javascript复制const usersGroupedByEmail = Object.groupBy(users, user => user.email);
const emailToSearch = "third@domain.com";
let foundUsers = usersGroupedByEmail[emailToSearch];
console.log(foundUsers);
// [{ id: 3, email: 'third@domain.com', language: 'CSS' }]
大成功!我们获得了与之前相同的结果,但无需编写循环。这意味着我们现在处于恒定时间复杂度,对吗?对吗?
其实并非完全如此。我们在这里做的一切就是去除了循环,而是通过调用带有要搜索的电子邮件的对象来实现。我们之所以能做到这一点,是因为 Object.groupBy 接受了一个对象列表(在这种情况下)和一个函数,该函数指定了我们要如何对数据进行分组。在这里,我们要根据电子邮件对用户进行分组,因此返回了电子邮件。
然而,在这种情况下,我们并没有改变算法的时间复杂度。如果我们拿这段代码进行基准测试,我们会发现它大致与先前的代码花费的时间相同。
那么Object.groupBy 是如何工作的呢?
简单来说,它通过循环遍历我们用户数组中的所有项。从那里开始,您可以开始猜测出了什么问题。
以下是其示例实现。
代码语言:javascript复制/**
* @param {Array<Item>} items
* @param {(item: Item) => string | number | symbol} getKey
* @return {{[key: string | number | symbol]: Array<Item>}}
*/
const groupBy = (items, getKey) => {
return items.reduce((grouped, item) => {
const key = getKey(item);
if (key in grouped) {
const previousItems = grouped[key];
return {
...grouped,
[key]: [
...previousItems,
item
]
};
}
return {
...grouped,
[key]: [item]
};
}, {});
}
因为它需要循环遍历我们所有的数据来构建一个对象,然后可以用于通过电子邮件访问我们的用户,所以它花费的时间实际上与您使用先前的解决方案或此解决方案的时间相同。
在这种特定情况下(我坚持这一点),使用 Object.groupBy 是没有用的。
那么为什么要麻烦呢?
实际上,这一切都取决于上下文。就像软件工程中的一切一样,目标是找到特定用例场景的最佳解决方案。
您不会为部署一个简单的 HTML 和 CSS 陆页使用 Kubernetes 集群,对吧?
在这里大致也是如此。在这个特定情况下,我们的分组(或索引)对象的有限使用使得首先将用户按电子邮件分组变得无用。我们本可以(多写一些代码)使用传统循环来完成。
然而,如果您现在要发出多个搜索请求,您会开始注意到使用分组对象要快得多。
因为访问 usersGroupedByEmailemailToSearch 是恒定时间。实际上,您可以将 Object.groupBy 的结果视为数据库中的索引表,它允许您以恒定时间访问数据,并降低了需要恒定访问诸如用户之类的数据的算法的时间复杂度。
因此,接下来的一百次搜索将只花费恒定时间,而如果您使用先前的循环搜索一百个用户,您将增加搜索一百个用户的时间,因为您需要遍历所有十亿用户一百次。这在最坏情况下仍然具有线性时间复杂度,但对于十亿用户,您将开始注意到算法中的某些减速。
要点
Object.groupBy 是 JavaScript 生态系统中的一项很棒的功能,因为它意味着对于这个特定的用例场景(在列中更快地搜索大量数据),您不需要下载一堆库来做到这一点(您可能以前已经使用 Ramda 或 Lodash)或者创建可能有缺陷的自己的版本,需要额外的测试来确保此算法的安全性。
但是,这并不是万能的解决方案,对于复杂的搜索,您需要的不仅仅是访问原始数据。例如,您可能希望允许对不区分大小写的完整文本进行搜索。
此外,分组操作是昂贵的,因为它需要线性时间来实现数据的索引化。此外,它需要一定的空间,因为您需要一种方式来引用您分组的用户。因此,您正在以空间换时间。对于十亿行数据,这可能是需要认真考虑的事情,特别是如果数据需要重新索引。
在这种情况下,就像对于模糊搜索一样,Object.groupBy 将毫无用处,因为它局限于精确匹配。这使得它在数据库索引和应用程序端的精确搜索方面非常棒。
那么你呢?您有没有想出 Object.groupBy 可以发挥作用的用例?在下面的评论区告诉我!
我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!