You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Ramda将扁平对象数组转换为嵌套对象数组

问题描述

我有如下数据结构:

[{fooId: '1', prop:'bla',prop2: 'bla', barId:'1', prop_1: 'bla', prop2_1: 'bla' },
{fooId: '1', prop:'bla',prop2: 'bla', barId:'2', prop_1: 'bla', prop2_1: 'bla' },
{fooId: '2', prop:'bla',prop2: 'bla', barId:'3', prop_1: 'bla', prop2_1: 'bla' },
{fooId: '2', prop:'bla',prop2: 'bla', barId:'4', prop_1: 'bla', prop2_1: 'bla' }]

需要用Ramda转换为如下结构:

[{fooId: '1', prop:'bla',prop2: 'bla', bars:[
{barId:'1', prop: 'bla', prop2: 'bla'},
{barId:'2', prop: 'bla', prop2: 'bla'}] },
{fooId: '2', prop:'bla',prop2: 'bla', bars:[
{barId:'3', prop: 'bla', prop2: 'bla'},
{barId:'4', prop: 'bla', prop2: 'bla'}] }
]

之前用R.pick实现过类似结构,但现在需要属性重命名,我写的reducer函数出了问题:所有Bar都被放到每个Foo里,而不是对应Foo。代码如下:

const transformQueryResult = r.pipe(
    r.groupBy(
        r.prop('fooId')
    ),
    r.map(r.reduce((enrichedFoo, fooWithBar) =>{
       enrichedFoo.fooId = fooWithBars.barId // 拼写错误:fooWithBars应为fooWithBar
       enrichedFoo.prop1 = fooWithBars.prop1 // 同样拼写错误,且原始数据里是prop/prop2,不是prop1
       enrichedFoo.prop2 = fooWithBars.prop2
       enrichedFoo.bars = r.append(
           {barId: fooWithBars.barId,
            prop1:fooWithBars.prop1_1, // 目标是prop/prop2,不是prop1
            prop2:fooWithBars.prop2_1
           }, enrichedFoo.bars || []
      );
      return enrichedFoo;
    },{} )
),
r.values
)

我是函数式编程新手,用循环很快能解决,但这里卡住了,求帮助。


解决方法

你的代码问题分析

  1. 拼写错误:reducer里把参数fooWithBar写成了fooWithBars,导致引用错误,可能意外共享变量或出现未定义问题。
  2. 属性名不匹配:原始数据中Foo的属性是prop/prop2,你代码里写的是prop1/prop2;目标结构里Bar的属性是prop/prop2,你代码里写的是prop1/prop2,完全不符合需求。
  3. reducer初始化逻辑粗糙:虽然用groupBy按fooId分了组,但reducer初始化空对象后重复赋值,不如直接基于分组内第一个元素构建Foo基础属性,避免冗余操作。

正确的Ramda实现方案

拆分步骤让逻辑更清晰:

const R = require('ramda');

// 提取单个Foo的基础属性(排除Bar相关字段)
const getFooBase = R.pick(['fooId', 'prop', 'prop2']);

// 把原始条目转换为目标Bar对象(完成属性重命名)
const toBar = R.converge(R.merge, [
  R.pick(['barId']),
  R.pipe(
    R.pick(['prop_1', 'prop2_1']),
    R.renameKeys({ prop_1: 'prop', prop2_1: 'prop2' })
  )
]);

// 处理同一fooId的分组数据
const processGroup = (items) => {
  const fooBase = getFooBase(R.head(items));
  const bars = R.map(toBar, items);
  return R.assoc('bars', bars, fooBase);
};

// 整体转换管道
const transformQueryResult = R.pipe(
  R.groupBy(R.prop('fooId')),
  R.map(processGroup),
  R.values
);

代码解释

  • getFooBase:用R.pick提取Foo的核心属性,同一分组内的Foo属性完全一致,取第一个元素即可。
  • toBar:通过R.converge合并两部分数据:一是原始的barId,二是把prop_1/prop2_1重命名为prop/prop2后的属性,最终得到目标Bar结构。
  • processGroup:对每个分组,先构建Foo基础对象,再把分组内所有条目转成Bar数组,最后用R.assoc把bars数组挂载到Foo对象上。
  • 整体管道:先按fooId分组,再逐个处理分组生成目标结构,最后用R.values把分组对象转成数组输出。

简化紧凑版本

如果想让代码更凝练,可以把逻辑合并到管道中:

const transformQueryResult = R.pipe(
  R.groupBy(R.prop('fooId')),
  R.map((group) => ({
    ...R.pick(['fooId', 'prop', 'prop2'], R.head(group)),
    bars: R.map(
      R.pipe(
        R.converge(R.merge, [
          R.pick(['barId']),
          R.pipe(
            R.pick(['prop_1', 'prop2_1']),
            R.renameKeys({ prop_1: 'prop', prop2_1: 'prop2' })
          )
        ])
      ),
      group
    )
  })),
  R.values
);

这样就能得到符合需求的结构,每个Foo只会对应自己的Bar数组。

内容的提问来源于stack exchange,提问作者Whateverdev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 14:26:14