如何将对象拆分为嵌套对象?(递归方式)

8

我有一个包含下划线(_)变量名的数据集,例如下面:

const data = {
   m_name: 'my name',
   m_address: 'my address',
   p_1_category: 'cat 1',
   p_1_name: 'name 1',
   p_2_category: 'cat 2',
   p_2_name: 'name 2'
}

我想将它们拆分成嵌套的对象/数组,以下是我想要的结果。

{
 m: {
     name: "my name",
     address: "my address"
 },
 p: {
    "1": {category: 'cat 1', name: 'name 1'}, 
    "2": {category: 'cat 2', name: 'name 2'}
 } 
}

我如何编写递归方法来实现它,而不是硬编码? 也许它应该允许处理更深层次嵌套的对象,例如“p_2_one_two_category:'value'”,转换成p:{2:{one:{two:category:'value'}}}。

var data ={
  m_name: 'my name',
  m_address: 'my address',
  p_1_category: 'cat 1',
  p_1_name: 'name 1',
  p_2_category: 'cat 2',
  p_2_name: 'name 2',
  p_2_contact: '1234567',
  k_id: 111,
  k_name: 'abc'
}

var o ={};
Object.keys(data).forEach(key => {
  var splited =  key.split(/_(.+)/);
  if (!o[splited[0]]) {
    o[splited[0]] = {};
  }
  var splited1 = splited[1].split(/_(.+)/);
  if (splited1.length < 3) {
    o[splited[0]][splited[1]] = data[key];
  } else {
    if (!o[splited[0]][splited1[0]]){ 
      o[splited[0]][splited1[0]] = {};
    }
    o[splited[0]][splited1[0]][splited1[1]] = data[key];
  }
});
console.log(o);


你的代码和Nenad答案中的代码都与你的样本输出略有不同。(m是一个纯对象,而不是单属性对象的数组。)这些结果似乎比你的样本输出更合理。这才是你真正想要的吗? - Scott Sauyet
@ScottSauyet 哎呀,我放错了结果,我已经更新了,但我的代码片段和他的一样。是的,那就是我想要的。谢谢。 - Devb
4个回答

10
你可以使用reduce方法,它将创建一个与仅为对象的嵌套结构类似的嵌套结构。
var data = {
  m_name: 'my name',
  m_address: 'my address',
  p_1_category: 'cat 1',
  p_1_name: 'name 1',
  p_2_category: 'cat 2',
  p_2_name: 'name 2',
  p_2_contact: '1234567',
  k_id: 111,
  k_name: 'abc'
}


const result = Object
  .entries(data)
  .reduce((a, [k, v]) => {
    k.split('_').reduce((r, e, i, arr) => {
      return r[e] || (r[e] = arr[i + 1] ? {} : v)
    }, a)

    return a
  }, {})

console.log(result)


2
我不知道那个输出格式是否真的是你要寻找的,还是你能够达到的最好水平。另一种选择是生成类似于这样的东西:
{
  m: {name: "my name", address: "my address"},
  p: [
    {category: "cat 1", name: "name 1"},
    {category: "cat 2", name: "name 2"}
  ]
}

这与您代码的输出有一个主要区别。现在,p是一个普通的对象数组,而不是一个以12为索引的对象。这可能对您没有帮助,但它是一个有趣的选择。还有第二个与您提供的示例输出不同的地方。您原来的代码和Nenad的答案都返回m: {name: "my name", address: "my address"},而不是请求的m: [{name: "my name"}, {address: "my address"}]。这对我来说更合乎逻辑,我也是这样做的。
下面是一些可以实现此功能的代码:

// Utility functions

const isInt = Number.isInteger

const path = (ps = [], obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const assoc = (prop, val, obj) => 
  isInt (prop) && Array .isArray (obj)
    ? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
    : {...obj, [prop]: val}

const assocPath = ([p = undefined, ...ps], val, obj) => 
  p == undefined
    ? obj
    : ps.length == 0
      ? assoc(p, val, obj)
      : assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)

// Main function

const hydrate = (flat) => 
  Object .entries (flat) 
    .map (([k, v]) => [k .split ('_'), v])
    .map (([k, v]) => [k .map (p => isNaN (p) ? p : p - 1), v])
    .reduce ((a, [k, v]) => assocPath (k, v, a), {})

// Test data

const data = {m_name: 'my name', m_address: 'my address', p_1_category: 'cat 1', p_1_name: 'name 1', p_2_category: 'cat 2', p_2_name: 'name 2' }

// Demo

console .log (
  hydrate (data)
)
.as-console-wrapper {min-height: 100% !important; top: 0}

这段代码受到了Ramda的启发(我是其中的一位作者)。实用函数pathassocassocPath具有类似于Ramda的API,但这些是独特的实现(借鉴自另一个答案)。由于这些被构建到Ramda中,如果您的项目使用Ramda,则只需要hydrate函数。
与Nenad(非常好!)答案之间的最大区别在于,我们的对象填充考虑了字符串键和数字键之间的差异,其中字符串键被认为是普通对象,而数字键被认为是数组。然而,由于这些被分离出我们最初的字符串(p_1_category),这可能会导致歧义,如果您有时可能希望那些成为对象。
我还使用了一个有点丑陋的技巧,可能无法适应其他数字值:我从数字中减去1,使得p_1_category中的1映射到零索引。如果您的输入数据看起来像p_0_category ... p_1_category而不是p_1_category ... p_2_category,那么我们可以跳过这个步骤。

无论如何,这很可能与您的基本要求相反,但对他人可能会有用。


1
我喜欢看到这里的不同方法。也许 path 可以是 ps.reduce((o = {}, p) => o[p], obj),它只是 ps.reduce(prop, obj) 吗? - Mulan
抱歉,我提供了错误的信息。正确的应该基于我的代码片段。顺便说一下,那很酷。将来可能会有用。谢谢。 - Devb
1
@感谢您:虽然path的那个版本可能适用于这种特定情况,但它失去了上面版本的空值安全性。同意答案的多样性;看来有趣的问题可以引出许多有趣的替代方案。 - Scott Sauyet

2

无需排序

你所提供的输出结果没有遵循一定的规律。有些项被分组成数组,而另一些项则被分组成对象。由于类似数组的对象的行为与数组相同,因此我们将使用对象。

此答案中的输出结果与Nenad的相同,但这个程序不需要事先对对象的键进行排序 -

const nest = (keys = [], value) =>
  keys.reduceRight((r, k) => ({ [k]: r }), value)

const result =
  Object
    .entries(data)
    .map(([ k, v ]) => nest(k.split("_"),  v))
    .reduce(merge, {})

console.log(result)

输出 -

{
  m: {
    name: "my name",
    address: "my address"
  },
  p: {
    1: {
      category: "cat 1",
      name: "name 1"
    },
    2: {
      category: "cat 2",
      name: "name 2",
      contact: "1234567"
    }
  },
  k: {
    id: 111,
    name: "abc"
  }
}

合并

我正在借用我在另一个答案中编写的通用merge函数。重复使用通用函数的好处很多,我不会在这里重复说明。如果您想了解更多,请阅读原始帖子-

const isObject = x =>
  Object (x) === x

const mut = (o = {}, [ k, v ]) =>
  (o[k] = v, o)

const merge = (left = {}, right = {}) =>
  Object.entries (right)
    .map
      ( ([ k, v ]) =>
          isObject(v) && isObject(left[k])
            ? [ k, merge (left[k], v) ]
            : [ k, v ]
      )
    .reduce(mut, left)

浅合并可以按预期工作 -

const x =
  [ 1, 2, 3, 4, 5 ]

const y =
  [  ,  ,  ,  ,  , 6 ]

const z =
  [ 0, 0, 0 ]

console.log(merge(x, y))
// [ 1, 2, 3, 4, 5, 6 ]

console.log(merge(y, z))
// [ 0, 0, 0, <2 empty items>, 6 ]

console.log(merge(x, z))
// [ 0, 0, 0, 4, 5, 6 ]

并且还支持深度合并 -
const x =
  { a: [ { b: 1 }, { c: 1 } ] }

const y =
  { a: [ { d: 2 }, { c: 2 }, { e: 2 } ] }

console.log(merge (x, y))
// { a: [ { b: 1, d: 2 }, { c: 2 }, { e: 2 } ] }

展开下面的代码片段,可以在您自己的浏览器中查看我们的结果 -

const isObject = x =>
  Object(x) === x

const mut = (o = {}, [ k, v ]) =>
  (o[k] = v, o)

const merge = (left = {}, right = {}) =>
  Object
    .entries(right)
    .map
      ( ([ k, v ]) =>
          isObject(v) && isObject(left[k])
            ? [ k, merge(left[k], v) ]
            : [ k, v ]
      )
    .reduce(mut, left)

const nest = (keys = [], value) =>
  keys.reduceRight((r, k) => ({ [k]: r }), value)

const data =
  {m_name:'my name',m_address:'my address',p_1_category:'cat 1',p_1_name:'name 1',p_2_category:'cat 2',p_2_name:'name 2',p_2_contact:'1234567',k_id:111,k_name:'abc'}

const result =
  Object
    .entries(data)
    .map(([ k, v ]) => nest(k.split("_"),  v))
    .reduce(merge, {})
  
console.log(JSON.stringify(result, null, 2))


0
使用forEach循环对象。
根据分隔符分割键并遍历数组
直到最后一个键,创建空对象并在指针/运行器中维护当前对象。
在最后一个键上,只需添加值。

const unflatten = (data, sep = "_") => {
  const result = {};
  Object.entries(data).forEach(([keys_str, value]) => {
    let runner = result;
    keys_str.split(sep).forEach((key, i, arr) => {
      if (i === arr.length - 1) {
        runner[key] = value;
      } else if (!runner[key]) {
        runner[key] = {};
      }
      runner = runner[key];
    });
  });
  return result;
};

const data ={
  m_name: 'my name',
  m_address: 'my address',
  p_1_category: 'cat 1',
  p_1_name: 'name 1',
  p_2_category: 'cat 2',
  p_2_name: 'name 2',
  p_2_contact: '1234567',
  k_id: 111,
  k_name: 'abc'
}

console.log(unflatten(data));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接