小技巧:使用 .reduce() 将数组转换为对象
反馈💬
AWS AI 直播!
我就不赘述冗长乏味的引言了,文章正文如下:
假设你有一个这样的数组:
[
{id: 1, category: "frontend", title: "All About That Sass"},
{id: 2, category: "backend", title: "Beam me up, Scotty: Apache Beam tips"},
{id: 3, category: "frontend", title: "Sanitizing HTML: Going antibactirial on XSS attacks"}
]
您希望获得一个以类别为键的对象,并将其映射到具有该类别的文章 ID,如下所示:
{
frontend: [1, 3],
backend: [2]
}
你可以利用我们的朋友Array.prototype.reduce来做这件事。
const posts = [
{id: 1, category: "frontend", title: "All About That Sass"},
{id: 2, category: "backend", title: "Beam me up, Scotty: Apache Beam tips"},
{id: 3, category: "frontend", title: "Sanitizing HTML: Going antibactirial on XSS attacks"}
];
const categoryPosts = posts.reduce((acc, post) => {
let {id, category} = post;
return {...acc, [category]: [...(acc[category] || []), id]};
}, {});
好的,我们来看看效果如何。
我把它想象reduce成把我的数组变成了一个管道。这个管道接收一个初始值,并将数组中的每个值作为单独的步骤应用到管道中,然后返回新值。这个从一步传递到下一步的值通常被称为累加器,因为它会累积管道中的变化。累加器的初始值作为第二个参数传递给它reduce。在本例中,它是一个空对象。那么,数组中的元素是如何应用到累加器的呢?这取决于你传递给它的reduce第一个参数的函数。该函数返回的值将用作累加器的新值。
(acc, post) => {
let {id, category} = post;
return {...acc, [category]: [...(acc[category] || [])], id]};
}
此函数以累加器作为第一个参数,以数组中的一个元素作为第二个参数。第一行使用对象解构将帖子的分类和 ID 提取到各自的变量中。这样做是为了方便我们使用简洁的变量名,使下一行代码更加清晰。
return {...acc, [category]: [...(acc[category] || [])], id]};
我在这里使用了很多 ES6 语法,可能不是每个人都熟悉,所以让我们深入了解一下。
return {...acc}
如果我们只返回这个值,实际上就等于返回了累加器的初始值,因为...前面的 `this` 被称为扩展运算符(spread)。在对象字面量中,它会获取给定对象的所有属性和值,并将它们放入新创建的对象中。所以上面这行代码的作用就是获取累加器的所有属性,并将它们放入我们返回的对象中。
return {...acc, [category]: [...(acc[category] || [])], id]};
接下来你可能会注意到这个[category]:语法。这是一个计算属性名。其理念是,你可以在对象字面量中定义一个属性,而无需预先知道属性名。在上面的代码行中,属性名就是类别名称。
我们希望这个属性最终包含一个数组,其中包含所有属于此分类的帖子的 ID,所以让我们来看看我们给这个属性赋予的值:
[...(acc[category] || [])], id]}
这里我们再次看到了扩展运算符,但这次是在数组字面量中。与对象扩展运算符类似,它会从给定的数组中提取所有值,并将它们如同写入数组字面量一样,插入到新创建的数组的相应位置。
这为我们提供了一种非常简洁的方式来定义一个数组,这个数组就是在另一个数组的基础上附加一个或多个值。
const a = [1, 2, 3];
const b = [...a, 4]; // b = [1, 2, 3, 4]
所以,在我们的帖子示例中,我们希望将帖子的 ID 添加到累加器已有的 ID 列表中,因此我们只需这样写:
[...acc[category], id]}
但是,如果我们的累加器中还没有该类别下的任何帖子怎么办?(所有类别在开始时都会如此)那么结果acc[category]就会是undefined,而扩展语法只适用于可迭代值,例如数组,所以我们会得到一个TypeError。
[...(acc[category] || [])], id]}
因此,我们改为使用表达式acc[category] || [],(用花括号括起来,这样扩展语法就能应用于整个表达式。该||运算符在第一个值是假值(实际上undefined就是假值)时返回第二个值,所以如果我们的累加器中没有任何具有给定类别的帖子,我们就展开空数组,这样在新 ID 之前就不会添加任何值。
那么,让我们把所有内容整合起来:
const posts = [
{id: 1, category: "frontend", title: "All About That Sass"},
{id: 2, category: "backend", title: "Beam me up, Scotty: Apache Beam tips"},
{id: 3, category: "frontend", title: "Sanitizing HTML: Going antibactirial on XSS attacks"}
];
const categoryPosts = posts.reduce((acc, post) => {
let {id, category} = post;
return {...acc, [category]: [...(acc[category] || []), id]};
}, {});
调用reduceposts 数组,并将空对象作为初始累加器,对于每个帖子,我们:
- 提取帖子 ID 和分类
- 获取累加器的所有现有属性,并应用返回值。
- 如果累加器中已经存在一个包含文章所属类别 ID 的数组,则将文章 ID 添加到该数组中。否则,为该类别创建一个新的空数组,并将文章 ID 添加到该数组中。
- 传递给函数的返回值
redused将用作数组中下一个帖子的累加器,并reduce在处理完所有帖子后返回。
反馈💬
这是我第一篇面向初学者的文章,非常欢迎大家提出任何建设性的反馈意见!我觉得这篇文章用了太多 ES6 语法,反而显得过于复杂,而且我还觉得有必要一一解释。我应该尽量减少语法的使用,只专注于使用 ES6reduce的核心概念。我可能还会写一篇更精简的版本,但目前就先这样吧。
对于精通函数式编程的人来说,这种用法reduce可能非常显而易见。但我大部分的编程生涯都花在了编写过程式和面向对象代码上。Map 对我来说很快就变得很直观,但我仍然时不时地会恍然大悟,发现它还有更多用途reduce。