NodeJS 任何人都可以请解释这个减少代码给我?

xiozqbni  于 2023-01-25  发布在  Node.js
关注(0)|答案(2)|浏览(219)

我目前正在学习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

但是当我看到第一个代码块时,我完全被抛到了一边。是我太笨了还是我错过了什么???
有没有人可以带我通过上面的代码的每一个迭代,同时解释它是如何做的,它在每一个回合做什么?提前感谢很多。

vaj7vani

vaj7vani1#

你正在触及reduce的一个大问题,虽然它是一个很好的函数,但它经常偏爱难以阅读的代码,这就是为什么我经常使用其他构造。
您的函数按属性对多个对象进行分组:

const data = [
  {category: 'catA', id: 1}, 
  {category: 'catA', id: 2},
  {category: 'catB', id: 3}
]
console.log(groupBy(data, 'category'))

我会给你

{
  catA: [{category: 'catA', id: 1}, {category: 'catA', id: 2}],
  catB: [{category: 'catB', id: 3}]
}

它通过在每一步中拆分acc对象并使用新数据重建它来实现这一点:

objectArray.reduce((acc, obj) => {
    const key = obj[property];        // get the data value (i.e. 'catA')
    const curGroup = acc[key] ?? [];  // get collector from acc or new array 

    // rebuild acc by copying all values, but replace the property stored 
    // in key with an updated array
    return { ...acc, [key]: [...curGroup, obj] };
  }, {});

您可能需要查看spread operator (...)coalesce operator (??)
下面是一个可读性更强的版本:

objectArray.reduce((groups, entry) => {
    const groupId = entry[property];
    if(!groups[groupId]){
      groups[groupId] = [];
    }
    groups[groupId].push(entry);
    return groups;
  }, {});

这是一个很好的例子,我更喜欢一个好的老for

function groupBy(data, keyProperty){
  const groups = {}
  for(const entry of data){
    const groupId = entry[keyProperty];
    if(!groups[groupId]){
      groups[groupId] = [];
    }
    groups[groupId].push(entry);
  }
  return groups;
}

几乎相同的行数,相同的缩进级别,更容易阅读,甚至稍微快一点(或a whole lot,取决于数据大小,这会影响扩展,但不会影响推送)。

vxf3dgd4

vxf3dgd42#

这段代码在accumulator中构建了一个对象,从{}(一个空对象)开始,对象中的每个属性都是数组中的一组元素:属性名称是组的键,属性值是组中元素的数组。
代码const curGroup = acc[key] ?? [];获取组acc[key]的当前数组,如果没有,则获取一个新的空数组。??是“空合并运算符”。如果该值不是nullundefined,则计算其第一个操作数;如果第一个操作数是nullundefined,则计算其第二个操作数。
到目前为止,我们知道obj[property]确定了被访问对象的键,curGroup是该键的当前值数组(根据需要创建)。
然后return { ...acc, [key]: [...curGroup, obj] };使用spread表示法创建一个新的累加器对象,该对象具有当前acc的所有属性(...acc),然后将具有key中的名称的属性添加或替换为包含累加器为该键所具有的任何先前值的新数组(curGroup)加上被访问的对象(obj),因为该对象在组中,因为我们从obj[property]得到了key
这里也是通过注解与代码相关的部分,为了清晰起见,我将创建一个新数组[...curGroup, obj]的部分与创建一个新accumulator对象的部分分开:

function groupBy(objectArray, property) {
    return objectArray.reduce(
        (acc, obj) => {
            // Get the value for the grouping property from this object
            const key = obj[property];

            // Get the known values array for that group, if any, or
            // a blank array if there's no property with the name in
            // `key`.
            const curGroup = acc[key] ?? [];

            // Create a new array of known values, adding this object
            const newGroup = [...curGroup, obj];

            // Create and return a new object with the new array, either
            // adding a new group for `key` or replacing the one that
            // already exists
            return { ...acc, [key]: newGroup };
        },
        /* The starting point, a blank object: */ {}
    );
}

值得注意的是,这段代码在很大程度上是根据functional programming编写的,它使用reduce而不是循环(当不使用reduce时,FP通常使用递归而不是循环),并且创建新的对象和数组而不是修改现有的对象和数组。
在函数式编程之外,代码的编写方式可能会非常不同,但是reduce是为函数式编程设计的,这就是一个例子。
只是FWIW,这里有一个版本没有使用FP或不变性(更多关于不变性下面):

function groupBy(objectArray, property) {
    // Create the object we'll return
    const result = {};
    // Loop through the objects in the array
    for (const obj of objectArray) {
        // Get the value for `property` from `obj` as our group key
        const key = obj[property];
        // Get our existing group array, if we have one
        let group = result[key];
        if (group) {
            // We had one, add this object to it
            group.push(obj);
        } else {
            // We didn't have one, create an array with this object
            // in it and store it on our result object
            result[key] = [obj];
        }
    }
    return result;
}

你在评论中说:
我理解扩展运算符,但它以这种方式与acc[key]一起使用是我仍然感到困惑的事情。
是的,return { ...acc, [key]: [...curGroup, obj] };中包含了很多东西。:-)它有两种扩展语法(...不是运算符,尽管它不是特别重要),加上计算属性名表示法([key]: ____)。让我们将其分为两条语句,以便更容易讨论:

const updatedGroup = [...curGroup, obj];
return { ...acc, [key]: updatedGroup };

TL;DR -它创建并返回一个 new 累加器对象,该对象包含前一个累加器对象的内容以及当前/更新组的新属性或更新属性。
下面是它的分解过程:

  • [...curGroup, obj]使用 iterable spread。Iterable spread将可迭代对象(如数组)的内容展开到数组文本或函数调用的参数列表中。在本例中,它展开到数组文本中:[...curGroup, obj]表示“创建一个新数组([]),在curGroup可迭代对象的开头(...curGroup)展开该对象的内容,并在末尾(, obj)添加一个新元素。
  • { ...acc, ____ }使用 object property spread。Object property spread将对象的属性扩展到一个新的对象文本中。表达式{ ...acc, _____ }表示“创建一个新对象({}),将acc的属性扩展到它(...acc)中,然后添加或更新属性(我现在只保留_____的部分)
  • x1米39英寸(在对象文本中)使用 computed property name syntax 将变量的 value 用作对象文本属性列表中的属性名称。因此,与{ example: value }不同,{ example: value }会创建实际名称为example的属性,计算属性名称语法将[]放在变量或其他表达式周围,并将结果用作属性名称。例如,const obj1 = { example: value };const key = "example"; const obj2 = { [key]: value };都创建一个名为example的对象,其值来自valuereduce代码使用[key]: updatedGroup]在新累加器中添加或更新一个属性,该累加器的名称来自key,其值为新组数组。

为什么要创建新的累加器对象(和新的组数组),而不仅仅是更新代码开头的那个数组?因为代码的编写避免了修改任何对象(数组或累加器)。它总是创建一个新的,而不是修改一个。为什么?这是“不可变编程,“编写只会创建新事物而不是修改现有事物的代码。在某些上下文中,不可变编程有很好的理由。它降低了代码库中某个地方的代码更改在其他地方产生意外后果的可能性。因为原始对象是不可变的(例如Mongoose中的一个),或者必须将其视为 * 就好像 * 它是不可变的(例如React或Vue中的状态对象)。在这个特定代码中,它是没有意义的,这只是风格。在这个过程完成之前,这些对象中没有一个是在任何地方共享的,而且它们中没有一个是真正不可变的。代码可以很容易地使用push向组数组添加对象,并使用acc[key] = updatedGroup;向accumulator对象添加/更新组。不可变编程有很好的用途。2函数式编程通常坚持不可变性(就我的理解;我没有深入研究FP)。

相关问题