简体   繁体   English

试图了解用于对象映射的reduce函数吗?

[英]Attempting to understand reduce functions for object mapping?

I'm going through this tutorial and it is using reduce to take a Array<Student> and turn it into a { [key: string]: Array<string | number> } 我正在阅读本教程 ,它使用reduce来获取Array<Student>并将其转换为{ [key: string]: Array<string | number> } { [key: string]: Array<string | number> } it has this expression in it. { [key: string]: Array<string | number> }里面有这个表达式。 This is the first time I have seen something like this so I want to check if I understand it right. 这是我第一次看到这样的内容,因此我想检查一下我是否正确。 This is the entire expression: 这是整个表达式:

    export interface Student {
        id: string;
        name: string;
        sex: 'Male' | 'Female';
        standard: number;
        quarterlyScore: number;
        halfyearlyScore: number;
        annualScore: number;
    }

    export function getStudentGraphData(students:Array<Student>): { [key: string]: Array<string |number> } {
        return students.reduce((
            { names: nArray, quarterly: qArray,halfyearly: hArray, annual: aArray },
            { name, quarterlyScore, halfyearlyScore,annualScore }) => {
            return {
                names: [...nArray, name],
                quarterly: [...qArray,quarterlyScore],
                halfyearly: [...hArray,halfyearlyScore],
                annual: [...aArray, annualScore]
            };
        }, { names: [], quarterly: [], halfyearly: [, annual: [] });
    }

IIUC this is the part we want to reduce to (The return value): IIUC这是我们要减少的部分(返回值):

 { names: nArray, quarterly: qArray, halfyearly: hArray, annual: aArray }

This is the student object: 这是学生对象:

{ name, quarterlyScore, halfyearlyScore, annualScore }

This is the actual reduction. 这是实际的减少。 It takes the array from the previous step and blows it up into the new array using the spread ( ... ) operator and then puts the parameter from the student object, like name at the end of the new array 它使用上一步中的数组,并使用spread( ... )运算符将其分解为新数组,然后将来自学生对象的参数放入name ,如name放在新数组的末尾

return {
            names: [...nArray, name],
            quarterly: [...qArray, quarterlyScore],
            halfyearly: [...hArray, halfyearlyScore],
            annual: [...aArray, annualScore]
        };

This is the initial value of the return value: 这是返回值的初始值:

{ names: [], quarterly: [], halfyearly: [], annual: [] }

Did I get that right approximately? 我大致正确吗?

Did I get that right approximately? 我大致正确吗?

Yes. 是。 Each call to the reduce callback creates a new object with the arrays from the original object expanded into new arrays that also have the name (etc.) of each object being visited. 每次对reduce回调的调用都会创建一个新对象,并将原始对象中的数组扩展为新数组,该数组还具有要访问的每个对象的name (等)。 So for every entry in students , the code creates and throws away five objects (the container object and four arrays). 因此,对于students每个条目,代码都会创建并丢弃五个对象(容器对象和四个数组)。

The for-of version, without all those temporary objects, would be: 没有所有这些临时对象for-of版将是:

const names = [];
const quarterly = [];
const halfyearly = [];
const annual = [];
for (const { name, quarterlyScore, halfyearlyScore, annualScore } of students) {
    names.push(name);
    quarterly.push(quarterlyScore);
    halfyearly.push(halfyearlyScore);
    annual.push(annualScore);
}
return {names, quarterly, halfyearly, annual};

Or you could do four map calls, if the students array isn't so long that it matters that you're making four passes through it instead of one (which it usually isn't): 或者,如果对students数组的使用时间不长,那么要进行四次而不是一次(通常不是一次),则可以进行四个map调用:

return {
    names: students.map(({name}) => name),
    quarterly: students.map(({quarterlyScore}) => quarterlyScore),
    halfyearly: students.map(({halfyearlyScore}) => halfyearlyScore),
    annual: students.map(({annualScore}) => annualScore)
};

Your solution is over-complicated, even for the reduce technique you use. 您的解决方案过于复杂,即使您使用的reduce技术也是如此。 The main culprit is the renaming you do of the accumulator properties, only to rename them again when you return a new accumulator. 罪魁祸首是对累加器属性进行重命名,只是在返回新的累加器时才对其重命名。 Easier is to just keep the accumulator names, especially as they don't conflict with your item property names. 更简单的方法是仅保留累加器名称,尤其是当它们与您的项目属性名称不冲突时。

This version cleans up that, removes the TS annotations (which you might have to add back; but they do tend to clutter things up), and replaces the outer function declaration with an arrow: 这个版本可以解决这个问题,删除TS注释(您可能必须添加回去;但是它们确实会使事情变得混乱),并用箭头替换外部函数声明:

 const getStudentGraphData = (students) => students.reduce( ( { names, quarterly, halfyearly, annual }, { name, quarterlyScore, halfyearlyScore, annualScore } ) => ({ names: [...names, name], quarterly: [...quarterly, quarterlyScore], halfyearly: [...halfyearly, halfyearlyScore], annual: [...annual, annualScore] }), { names: [], quarterly: [], halfyearly: [], annual: [] } ) const students = [{id: 1, name: 'Barney', sex: 'Male', quarterlyScore: 83, halfyearlyScore: 88, annualScore: 91}, {id: 2, name: 'Betty', sex: 'Female', quarterlyScore: 92, halfyearlyScore: 89, annualScore: 95}, {id: 3, name: 'Fred', sex: 'Male', quarterlyScore: 69, halfyearlyScore: 73, annualScore: 68}, {id: 4, name: 'Wilma', sex: 'Female', quarterlyScore: 85, halfyearlyScore: 78, annualScore: 80}] console.log(getStudentGraphData(students)) 

But sometimes the right abstraction can clean things up on its own. 但是有时候正确的抽象可以自己清理。 Here's another version that ends up working more like the multiple map versions, but abstracts that to a more declarative function: 这是另一个版本,其最终工作方式类似于多个map版本,但将其抽象为更具声明性的功能:

 const collect = (fields) => (objs) => Object.entries(fields).reduce( (a, [k, v]) => ({...a, [k]: objs.map(o => o[v])}), {} ) const getStudentGraphData = collect({ names: 'name', quarterly: 'quarterlyScore', halfyearly: 'halfyearlyScore', annual: 'annualScore', }) const students = [{id: 1, name: 'Barney', sex: 'Male', quarterlyScore: 83, halfyearlyScore: 88, annualScore: 91}, {id: 2, name: 'Betty', sex: 'Female', quarterlyScore: 92, halfyearlyScore: 89, annualScore: 95}, {id: 3, name: 'Fred', sex: 'Male', quarterlyScore: 69, halfyearlyScore: 73, annualScore: 68}, {id: 4, name: 'Wilma', sex: 'Female', quarterlyScore: 85, halfyearlyScore: 78, annualScore: 80}] console.log(getStudentGraphData(students)) 

The collect function might be useful in other places in a codebase, but even if it's not, the now more declarative version of getStudentGraphData might make it worth adding collect . collect函数可能在代码库中的其他位置很有用,但是即使不是, getStudentGraphData声明性版本也可能值得添加collect

The only API question I had in creating collect was how to decide whether the target names ( names , quarterly , etc.) should be the keys and the source name ( name , quarterlyScore , etc.) the values of the configuration object or vice versa. 我创建collect时唯一的API问题是如何确定目标名称( namesquarterly ,等)应为键,源名称( namequarterlyScore ,等)应为配置对象的值,反之亦然。 This feels slightly more correct, but either version makes getStudentGraphData much more understandable. 感觉稍微更正确,但是任何一个版本都使getStudentGraphData更加容易理解。

Update 更新资料

For some reason, this has been stuck in my head. 由于某种原因,这一直困扰着我。 I keep thinking about what sort of API something like collect should have. 我一直在思考collect应该具有什么样的API。 While I'm pretty happy with that last version, here is a significantly different one, and one which allows for no confusion about what is being collected and what those results are called: 虽然我对最后一个版本感到非常满意,但这是一个截然不同的版本,并且不会混淆正在收集的内容和这些结果称为什么:

 const collect = field => { let fields = [[field, field]] const fn = (objs) => fields.reduce( (a, [k, v]) => ({...a, [k]: objs.map(o => o[v])}), {} ) fn.and = (field) => { fields.push([field, field]) return fn } fn.as = (field) => { fields[fields.length - 1][0] = field return fn; } return fn; } const getStudentGraphData = collect('id') .and('name') .and('quarterlyScore').as('quarterly') .and('halfyearlyScore').as('halfyearly') .and('annualScore').as('annual') const students = [{id: 1, name: 'Barney', sex: 'Male', quarterlyScore: 83, halfyearlyScore: 88, annualScore: 91}, {id: 2, name: 'Betty', sex: 'Female', quarterlyScore: 92, halfyearlyScore: 89, annualScore: 95}, {id: 3, name: 'Fred', sex: 'Male', quarterlyScore: 69, halfyearlyScore: 73, annualScore: 68}, {id: 4, name: 'Wilma', sex: 'Female', quarterlyScore: 85, halfyearlyScore: 78, annualScore: 80}] console.log(getStudentGraphData(students)) 

This is a somewhat unusual technique I've only used a few times in production code, but it works well, and the definition of getStudentGraphData is about as readable as I can imagine. 我在生产代码中只使用过几次这种有点不寻常的技术,但是它运作良好,而且getStudentGraphData的定义几乎可以想象到。


My take on reduce is that it is powerful and necessary, but I try not to use it if I can use map , filter , find , some , every , or the like. 我对reduce看法是它功能强大且必要,但是如果可以使用mapfilterfindsomeevery或类似的东西,我尽量不要使用它。 Those give clear explanations of what you're doing in the code. 这些可以清楚地说明您在代码中的工作。 reduce is more like a for -loop: often not self-explanatory. reduce更像是for loop:通常不言自明。

Would be cool to see answers that are more efficient and more elegant. 看到更有效,更优雅的答案会很酷。

I guess a more efficient (not sure about "elegant") approach would be to make a generic function that abstracts the computation at hand and doesn't limit itself to a particular data structure. 我猜想,一种更有效(不确定“优雅”的方法)的方法将是创建一个通用函数,该函数将手头的计算抽象化,并且不将自身局限于特定的数据结构。 For example: 例如:

 let collect = objects => { let res = {}; for (let obj of objects) for (let [k, v] of Object.entries(obj)) res[k] = (res[k] || []).concat(v) return res; }; // students = [ {id:1, name:'1', sex:'X', standard: 1, propName: 11, anotherSillyPropName:111,}, {id:2, name:'2', sex:'Y', standard: 2, propName: 22, anotherSillyPropName:222,}, {id:3, name:'3', sex:'Z', standard: 3, propName: 33, anotherSillyPropName:333,}, ] console.log(collect(students)) 

Note that our collect doesn't know anything about "students" and their "scores", it simply transposes a matrix of values. 请注意,我们的collect对于“学生”及其“分数”一无所知,它只是对值矩阵进行转置。 So you can reuse it everywhere you need such functionality. 因此,您可以在需要此类功能的任何地方重用它。

On a general note, if your domain-specific functions like getStudentGraphData tend to grow longer than a couple of lines, it's a strong indication that the underlying computation must be factored out and separated from the specific problem domain. 总的来说,如果像getStudentGraphData这样的特定于域的函数的增长往往超过几行,则有力地表明必须将基础计算从特定问题域中分解出来。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM