如何将嵌套对象的层次结构转换为自身的平面表示,每个扁平键反映其原始 属性 路径?

How does one transform a nested object's hierarchy into a flat representation of itself with each flattened key reflecting its original property path?

我有一个 JSON 符合这样的对象结构...

{
  "Name": "Pedro Smith",
  "Age": "8",
  "Parents": {
    "Mother": {
      "Name": "Maria Smith",
      "RG": "123456"
    },
    "Father": {
      "Name": "Jose Smith",
      "RG": "5431"
    }
  }
}

...我想将上述对象处理成其自身的平面表示。新对象不应嵌套,但其以前的层次结构应通过其 key/property 名称反映,其中每个新键均按其最深值的 属性 路径聚合。

上面给定示例的预期结构应如下所示...

{
  "Name": "Pedro Smith",
  "Age": "8",
  "Parents-Mother-Name": "Maria Smith",
  "Parents-Mother-RG": "123456",
  "Parents-Father-Name": "Jose Smith",
  "Parents-Father-RG": "5431"
}

如何在JavaScript中完成这样的改造任务?

TLDR

  • 最终的、基于递归的方法在最后一个示例中给出。
  • 由于 OP 似乎是新手,因此将有 3 个代码迭代步骤,每个步骤都有解释和文档链接,以实现最终解决方案。

如果开始考虑非嵌套对象结构的解决方案或如何检索对象的第一层 key-value(又名entries) one might think, that iterating over such entries and somehow executing a logging forEach 键值对 可能是第一个有效的 step/approach。

并且由于人们不想使用 for...in 之类的东西而是更易读的东西,示例代码最终可能看起来像这样 ...

const sample = {
  Name: "Pedro Smith",
  Age: "8",
  Parents: {
    Mother: {
      Name: "Maria Smith",
      RG: "123456",
    },
    Father: {
      Name: "Jose Smith",
      RG: "5431",
    },
  },
};

Object
  .entries(sample)
  .forEach(entry => {

    const key = entry[0];
    const value = entry[1];

    console.log(key + ': ' + value + '\n');
  });
.as-console-wrapper { min-height: 100%!important; top: 0; }

当然可以通过不记录每个迭代步骤而是通过汇总最终结果来改进上述代码,这可以通过例如reduce, join and some Destructuring assignments ...

的帮助

const sample = {
  Name: "Pedro Smith",
  Age: "8",
  Parents: {
    Mother: {
      Name: "Maria Smith",
      RG: "123456",
    },
    Father: {
      Name: "Jose Smith",
      RG: "5431",
    },
  },
};

console.log(
  Object
    .entries(sample)
    .reduce((result, [key, value]) => [

      result,
      key + ': ' + value,
    
    ].join('\n'), '')
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

但是 OP 既需要扁平化的对象结构,也需要聚合键,后者应该以某种方式将键路径反映到每个相似的子结构中。因此,需要走对象 tree/structure.

为了做到这一点,并且由于上面的示例已经提供了一个简单对象分支的解决方案,因此需要将 reduce 函数转换为 recursive 函数。

The aggregating/accumulating first argument of reduce will be used for collecting and configuration purposes like not just carrying the result reference but also referring to the current's key (aggregated by a Template literal) prefixconnector 字符串 ...

function recursivelyConcatKeyAndAssignFlatEntry(collector, [key, value]) {
  const { connector, keyPath, result } = collector;

  // concatenate/aggregate the current
  // but flattened key if necessary.
  key = keyPath
    ? `${ keyPath }${ connector }${ key }`
    : key;

  if (value && (typeof value === 'object')) {
    // ... recursively continue flattening in case
    // the current `value` is an "object" type ...
    Object
      .entries(value)
      .reduce(recursivelyConcatKeyAndAssignFlatEntry, {
        connector,
        keyPath: key,
        result,
      });
  } else {
    // ... otherwise ... just assign a new flattened
    // key and current value pair to the final object.
    result[key] = value;
  }
  return collector;
}

const sample = {
  Name: "Pedro Smith",
  Age: "8",
  Parents: {
    Mother: {
      Name: "Maria Smith",
      RG: "123456",
    },
    Father: {
      Name: "Jose Smith",
      RG: "5431",
    },
  },
};

console.log(
  Object
    .entries(sample)
    .reduce(recursivelyConcatKeyAndAssignFlatEntry, {

      connector: '-',
      keyPath: '',
      result: {},

    }).result
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

为了解决这个问题,我们将采用递归的方法:

  1. 通过将对象 oldObject 中的所有 key/value 对与 for..in 循环遍历对象。
  2. 检查这些值是否也是具有运算符 typeof 等于对象的对象 如果是,我们将再次遍历该对象中的 key/value 对,但通过再次调用相同的函数进行递归flattenObject.
  3. 直到我们取出所有与 - 连接的嵌套键,以及它们相应的值并将它们分配给我们返回的对象 flattenedObject
  4. 否则,如果键 innerObj 的值不是一个对象,直接将它和它的值一起存储在结果对象 flattenedObject 中。
const flattenObject = (oldObject) =>{
    
    const flattenedObject  = {};

    for (const innerObj in oldObject) {

        if ((typeof oldObject[innerObj]) === 'object') {
                const newObject = flattenObject(oldObject[innerObj]);
                for (const key in newObject) {
                    flattenedObject[innerObj + '-' + key] = newObject[key];
                }
            }
        else {
            flattenedObject[innerObj] = oldObject[innerObj];
        }
    }
    return flattenedObject;
};

测试:

const obj = {
  "Name": "Pedro Smith",
  "Age": "8",
  "Parents": {
    "Mother": {
      "Name": "Maria Smith",
      "RG": "123456"
    },
    "Father": {
      "Name": "Jose Smith",
      "RG": "5431"
    }
  }
}
console.log(flattenObject(obj));

结果:

{
    "Name": "Pedro Smith",
    "Age": "8",
    "Parents-Mother-Name": "Maria Smith",
    "Parents-Mother-RG": "123456",
    "Parents-Father-Name": "Jose Smith",
    "Parents-Father-RG": "5431"
}

由于这个问题最近被重新提出,以下是我认为构建在可重用 pathEntries 函数之上的稍微简单的方法:

const pathEntries = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (
        ([k, x]) => pathEntries (x) .map (
          ([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v]
        )
      ) 
    : [[[], obj]]

const transform = (o) =>
  pathEntries (o) 
    .reduce ((a, [k, v]) => ((a[k .join ('-')] = v), a), {})

const person = {Name: "Pedro Smith", Age: "8", Parents: {Mother: {Name: "Maria Smith", RG: "123456"}, Father: {Name: "Jose Smith", RG: "5431"}}}

console .log (transform (person))

pathEntries 将输入转换为这种灵活的格式:

[
  [["Name"], "Pedro Smith"], 
  [["Age"], "8"], 
  [["Parents", "Mother", "Name"], "Maria Smith"], 
  [["Parents", "Mother", "RG"], "123456"], 
  [["Parents", "Father", "Name"], "Jose Smith"], 
  [["Parents", "Father", "RG"], "5431"]
]

transform 调用 pathEntries,然后通过使用 '-'.

连接路径将结果折叠到单个对象中

不过,一个建议是您可能希望寻找 '-' 以外的其他分隔符。很多人用'.',我也见过'|'。问题是如果你想添加一个 属性 就像 'Mother-In-Law' 那样会产生歧义。 'Parents-Mother-In-Law-Name': 'Clara Petrie'是否建议

{
  // ...
  Parents: {
    // Mother, Father
    'Mother-In-Law': {Name: 'Clara Petrie'}
    //
  }
}

还是像这样混入'Mother':

{
  // ...
  Parents: {
    Mother {
      Name: 'Marie Smith',
      RG: '123456',
      In: {
        Law: {
          Name: 'Clara Petrie'
        }
      }
    // Father
  }
}

?没有完美的分隔符,但连字符更容易产生歧义。