我目前正在学习JS中的reduce方法,虽然我对它有一个基本的了解,但更复杂的代码完全把我搞糊涂了。我似乎无法理解代码是如何做它正在做的事情的。请注意,不是代码错了,而是我无法理解它。下面是一个例子:
const people = [
{ name: "Alice", age: 21 },
{ name: "Max", age: 20 },
{ name: "Jane", age: 20 },
];
function groupBy(objectArray, property) {
return objectArray.reduce((acc, obj) => {
const key = obj[property];
const curGroup = acc[key] ?? [];
return { ...acc, [key]: [...curGroup, obj] };
}, {});
}
const groupedPeople = groupBy(people, "age");
console.log(groupedPeople);
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }
现在,我所理解的reduce方法,接受一个数组,以顺序的方式对数组的所有元素运行一些提供的函数,并将每次迭代的结果添加到累加器中。非常简单。但是上面的代码似乎也对累加器做了一些事情,我似乎无法理解它。
acc[key] ?? []
做什么?
这样的代码使它看起来像是一件轻而易举的事:
const array1 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue
);
console.log(sumWithInitial);
// Expected output: 10
但是当我看到第一个代码块时,我完全被抛到了一边。是我太笨了还是我错过了什么???
有没有人可以带我通过上面的代码的每一个迭代,同时解释它是如何做的,它在每一个回合做什么?提前感谢很多。
2条答案
按热度按时间vaj7vani1#
你正在触及reduce的一个大问题,虽然它是一个很好的函数,但它经常偏爱难以阅读的代码,这就是为什么我经常使用其他构造。
您的函数按属性对多个对象进行分组:
我会给你
它通过在每一步中拆分
acc
对象并使用新数据重建它来实现这一点:您可能需要查看spread operator (...)和coalesce operator (??)
下面是一个可读性更强的版本:
这是一个很好的例子,我更喜欢一个好的老
for
:几乎相同的行数,相同的缩进级别,更容易阅读,甚至稍微快一点(或a whole lot,取决于数据大小,这会影响扩展,但不会影响推送)。
vxf3dgd42#
这段代码在accumulator中构建了一个对象,从
{}
(一个空对象)开始,对象中的每个属性都是数组中的一组元素:属性名称是组的键,属性值是组中元素的数组。代码
const curGroup = acc[key] ?? [];
获取组acc[key]
的当前数组,如果没有,则获取一个新的空数组。??
是“空合并运算符”。如果该值不是null
或undefined
,则计算其第一个操作数;如果第一个操作数是null
或undefined
,则计算其第二个操作数。到目前为止,我们知道
obj[property]
确定了被访问对象的键,curGroup
是该键的当前值数组(根据需要创建)。然后
return { ...acc, [key]: [...curGroup, obj] };
使用spread表示法创建一个新的累加器对象,该对象具有当前acc
的所有属性(...acc
),然后将具有key
中的名称的属性添加或替换为包含累加器为该键所具有的任何先前值的新数组(curGroup
)加上被访问的对象(obj
),因为该对象在组中,因为我们从obj[property]
得到了key
。这里也是通过注解与代码相关的部分,为了清晰起见,我将创建一个新数组
[...curGroup, obj]
的部分与创建一个新accumulator对象的部分分开:值得注意的是,这段代码在很大程度上是根据functional programming编写的,它使用
reduce
而不是循环(当不使用reduce
时,FP通常使用递归而不是循环),并且创建新的对象和数组而不是修改现有的对象和数组。在函数式编程之外,代码的编写方式可能会非常不同,但是
reduce
是为函数式编程设计的,这就是一个例子。只是FWIW,这里有一个版本没有使用FP或不变性(更多关于不变性下面):
你在评论中说:
我理解扩展运算符,但它以这种方式与
acc
和[key]
一起使用是我仍然感到困惑的事情。是的,
return { ...acc, [key]: [...curGroup, obj] };
中包含了很多东西。:-)它有两种扩展语法(...
不是运算符,尽管它不是特别重要),加上计算属性名表示法([key]: ____
)。让我们将其分为两条语句,以便更容易讨论:TL;DR -它创建并返回一个 new 累加器对象,该对象包含前一个累加器对象的内容以及当前/更新组的新属性或更新属性。
下面是它的分解过程:
[...curGroup, obj]
使用 iterable spread。Iterable spread将可迭代对象(如数组)的内容展开到数组文本或函数调用的参数列表中。在本例中,它展开到数组文本中:[...curGroup, obj]
表示“创建一个新数组([]
),在curGroup
可迭代对象的开头(...curGroup
)展开该对象的内容,并在末尾(, obj
)添加一个新元素。{ ...acc, ____ }
使用 object property spread。Object property spread将对象的属性扩展到一个新的对象文本中。表达式{ ...acc, _____ }
表示“创建一个新对象({}
),将acc
的属性扩展到它(...acc
)中,然后添加或更新属性(我现在只保留_____
的部分){ example: value }
不同,{ example: value }
会创建实际名称为example
的属性,计算属性名称语法将[]
放在变量或其他表达式周围,并将结果用作属性名称。例如,const obj1 = { example: value };
和const key = "example"; const obj2 = { [key]: value };
都创建一个名为example
的对象,其值来自value
。reduce
代码使用[key]: updatedGroup]
在新累加器中添加或更新一个属性,该累加器的名称来自key
,其值为新组数组。为什么要创建新的累加器对象(和新的组数组),而不仅仅是更新代码开头的那个数组?因为代码的编写避免了修改任何对象(数组或累加器)。它总是创建一个新的,而不是修改一个。为什么?这是“不可变编程,“编写只会创建新事物而不是修改现有事物的代码。在某些上下文中,不可变编程有很好的理由。它降低了代码库中某个地方的代码更改在其他地方产生意外后果的可能性。因为原始对象是不可变的(例如Mongoose中的一个),或者必须将其视为 * 就好像 * 它是不可变的(例如React或Vue中的状态对象)。在这个特定代码中,它是没有意义的,这只是风格。在这个过程完成之前,这些对象中没有一个是在任何地方共享的,而且它们中没有一个是真正不可变的。代码可以很容易地使用
push
向组数组添加对象,并使用acc[key] = updatedGroup;
向accumulator对象添加/更新组。不可变编程有很好的用途。2函数式编程通常坚持不可变性(就我的理解;我没有深入研究FP)。