将平面对象数组转换为嵌套对象

时间:2019-02-06 15:44:05

标签: javascript typescript recursion nested

我有以下数组(实际上是来自后端服务):

const flat: Item[] = [
    { id: 'a', name: 'Root 1', parentId: null },
    { id: 'b', name: 'Root 2', parentId: null },
    { id: 'c', name: 'Root 3', parentId: null },

    { id: 'a1', name: 'Item 1', parentId: 'a' },
    { id: 'a2', name: 'Item 1', parentId: 'a' },

    { id: 'b1', name: 'Item 1', parentId: 'b' },
    { id: 'b2', name: 'Item 2', parentId: 'b' },
    { id: 'b2-1', name: 'Item 2-1', parentId: 'b2' },
    { id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },
    { id: 'b3', name: 'Item 3', parentId: 'b' },

    { id: 'c1', name: 'Item 1', parentId: 'c' },
    { id: 'c2', name: 'Item 2', parentId: 'c' }
];

其中Item是:

interface Item {
    id: string;
    name: string;
    parentId: string;
};

为了与显示树(类似于文件夹)视图的组件兼容,需要将其转换为:

const treeData: NestedItem[] = [
    {
        id: 'a',
        name: 'Root 1',
        root: true,
        count: 2,
        children: [
          {
            id: 'a1',
            name: 'Item 1'
          },
          {
            id: 'a2',
            name: 'Item 2'
          }
        ]
    },
    {
        id: 'b',
        name: 'Root 2',
        root: true,
        count: 5, // number of all children (direct + children of children)
        children: [
          {
            id: 'b1',
            name: 'Item 1'
          },
          {
            id: 'b2',
            name: 'Item 2',
            count: 2,
            children: [
                { id: 'b2-1', name: 'Item 2-1' },
                { id: 'b2-2', name: 'Item 2-2' },
            ]
          },
          {
            id: 'b3',
            name: 'Item 3'
          },
        ]
    },
    {
        id: 'c',
        name: 'Root 3',
        root: true,
        count: 2,
        children: [
          {
            id: 'c1',
            name: 'Item 1'
          },
          {
            id: 'c2',
            name: 'Item 2'
          }
        ]
    }
];

其中NestedItem是:

interface NestedItem {
    id: string;
    name: string;
    root?: boolean;
    count?: number;
    children?: NestedItem[];
}

到目前为止,我所尝试的一切都是这样的:

// Get roots first
const roots: NestedItem[] = flat
    .filter(item => !item.parentId)
    .map((item): NestedItem => {
        return { id: item.id, name: item.name, root: true }
    });

// Add "children" to those roots
const treeData = roots.map(node => {
    const children = flat
        .filter(item => item.parentId === node.id)
        .map(item => {
            return { id: item.id, name: item.name }
        });
    return {
        ...node,
        children,
        count: node.count ? node.count + children.length : children.length
    }
});

但是,这当然只会获得第一级子级(根节点的直接子级)。它某种程度上需要递归,但我不知道如何实现。

6 个答案:

答案 0 :(得分:1)

不对展平数组的顺序或嵌套对象的深度进行任何假设:

Array.prototype.reduce具有足够的灵活性来完成此操作。如果您不熟悉Array.prototype.reduce,建议您使用reading this。您可以通过执行以下操作来完成此操作。

我有两个依赖递归的函数:findParentcheckLeftOversfindParent尝试找到父对象,并根据是否找到对象返回truefalse。在减速器中,如果findParent返回false,则将当前值添加到剩余数组中。如果findParent返回true,我打电话给checkLeftOvers,看看我剩余数组中的任何对象是否是刚刚添加的对象findParent的子对象。

注意:我在{ id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'}数组中添加了flat,以证明它会深入到您想要的深度。我还对flat进行了重新排序,以证明在这种情况下也可以使用。希望这会有所帮助。

const flat = [
    { id: 'a2', name: 'Item 1', parentId: 'a' },
    { id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'},
    { id: 'a1', name: 'Item 1', parentId: 'a' },
    { id: 'a', name: 'Root 1', parentId: null },
    { id: 'b', name: 'Root 2', parentId: null },
    { id: 'c', name: 'Root 3', parentId: null },
    { id: 'b1', name: 'Item 1', parentId: 'b' },
    { id: 'b2', name: 'Item 2', parentId: 'b' },
    { id: 'b2-1', name: 'Item 2-1', parentId: 'b2' },
    { id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },
    { id: 'b3', name: 'Item 3', parentId: 'b' },
    { id: 'c1', name: 'Item 1', parentId: 'c' },
    { id: 'c2', name: 'Item 2', parentId: 'c' }
];

function checkLeftOvers(leftOvers, possibleParent){
  for (let i = 0; i < leftOvers.length; i++) {
    if(leftOvers[i].parentId === possibleParent.id) {
      delete leftOvers[i].parentId
      possibleParent.children ? possibleParent.children.push(leftOvers[i]) : possibleParent.children = [leftOvers[i]]
      possibleParent.count = possibleParent.children.length
      const addedObj = leftOvers.splice(i, 1)
      checkLeftOvers(leftOvers, addedObj[0])
    }
  }
}

function findParent(possibleParents, possibleChild) {
  let found = false
  for (let i = 0; i < possibleParents.length; i++) {
    if(possibleParents[i].id === possibleChild.parentId) {
      found = true
      delete possibleChild.parentId
      if(possibleParents[i].children) possibleParents[i].children.push(possibleChild)
      else possibleParents[i].children = [possibleChild]
      possibleParents[i].count = possibleParents[i].children.length
      return true
    } else if (possibleParents[i].children) found = findParent(possibleParents[i].children, possibleChild)
  } 
  return found;
}
 
 const nested = flat.reduce((initial, value, index, original) => {
   if (value.parentId === null) {
     if (initial.left.length) checkLeftOvers(initial.left, value)
     delete value.parentId
     value.root = true;
     initial.nested.push(value)
   }
   else {
      let parentFound = findParent(initial.nested, value)
      if (parentFound) checkLeftOvers(initial.left, value)
      else initial.left.push(value)
   }
   return index < original.length - 1 ? initial : initial.nested
 }, {nested: [], left: []})
 
console.log(nested)

答案 1 :(得分:1)

假定平面项目数组始终按照您的情况进行排序(父级节点排在子节点之前)。下面的代码可以完成工作。

首先,我使用数组上的reduce来构建不具有count属性的树,以构建一个映射来跟踪每个节点并将父级链接到子级:

type NestedItemMap = { [nodeId: string]: NestedItem };

let nestedItemMap: NestedItemMap = flat
    .reduce((nestedItemMap: NestedItemMap, item: Item): NestedItemMap => {

        // Create the nested item
        nestedItemMap[item.id] = {
            id: item.id,
            name: item.name
        }

        if(item.parentId == null){
            // No parent id, it's a root node
            nestedItemMap[item.id].root = true;
        }
        else{
            // Child node
            let parentItem: NestedItem = nestedItemMap[item.parentId];

            if(parentItem.children == undefined){
                // First child, create the children array
                parentItem.children = [];
                parentItem.count = 0;

            }

            // Add the child node in it's parent children
            parentItem.children.push(
                nestedItemMap[item.id]
            );
            parentItem.count++;
        }

        return nestedItemMap;
    }, {});

缩小数组时,父节点始终排在第一位这一事实确保了在构建子节点时nestedItemMap中可以使用父节点。

这里有树,但是没有count属性:

let roots: NestedItem[] = Object.keys(nestedItemMap)
    .map((key: string): NestedItem => nestedItemMap[key])
    .filter((item: NestedItem): boolean => item.root);

要填充count属性,我个人更喜欢对树执行后深度优先搜索。但在您的情况下,要感谢节点ID的命名(排序后,父节点ID排在第一位)。您可以使用以下方法进行计算:

let roots: NestedItem[] = Object.keys(nestedItemMap)
    .map((key: string): NestedItem => nestedItemMap[key])
    .reverse()
    .map((item: NestedItem): NestedItem => {
        if(item.children != undefined){
            item.count = item.children
                .map((child: NestedItem): number => {
                    return 1 + (child.count != undefined ? child.count : 0);
                })
                .reduce((a, b) => a + b, 0);
        }

        return item;
    })
    .filter((item: NestedItem): boolean => item.root)
    .reverse();

我只是反转数组以首先获得所有子项(例如在后置DFS中),然后计算count值。 最后一个相反的地方就是按照您的问题进行排序:)。

答案 2 :(得分:1)

您可以为树的标准方法,该树采用一个循环并存储子级与父级之间以及父级与子级之间的关系。

要具有根属性,您需要进行其他检查。

然后采用迭代和递归的方法获取计数。

var data = [{ id: 'a', name: 'Root 1', parentId: null }, { id: 'b', name: 'Root 2', parentId: null }, { id: 'c', name: 'Root 3', parentId: null }, { id: 'a1', name: 'Item 1', parentId: 'a' }, { id: 'a2', name: 'Item 1', parentId: 'a' }, { id: 'b1', name: 'Item 1', parentId: 'b' }, { id: 'b2', name: 'Item 2', parentId: 'b' }, { id: 'b3', name: 'Item 3', parentId: 'b' }, { id: 'c1', name: 'Item 1', parentId: 'c' }, { id: 'c2', name: 'Item 2', parentId: 'c' }, { id: 'b2-1', name: 'Item 2-1', parentId: 'b2' }, { id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },],
    tree = function (data, root) {

        function setCount(object) {
            return object.children
                ? (object.count = object.children.reduce((s, o) => s + 1 + setCount(o), 0))
                : 0;
        }                

        var t = {};
        data.forEach(o => {
            Object.assign(t[o.id] = t[o.id] || {}, o);
            t[o.parentId] = t[o.parentId] || {};
            t[o.parentId].children = t[o.parentId].children || [];
            t[o.parentId].children.push(t[o.id]);
            if (o.parentId === root) t[o.id].root = true;          // extra
        });

        setCount(t[root]);                                         // extra
        return t[root].children;
    }(data, null);

console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

答案 3 :(得分:1)

也许可以为您提供帮助,输入是纯obj

nestData = (data, parentId = '') => {
return data.reduce((result, fromData) => {
  const obj = Object.assign({}, fromData);

  if (parentId === fromData.parent_id) {
    const children = this.nestData(data, fromData.id);
    if (children.length) {
      obj.children = children;
    } else {
      obj.userData = [];
    }
    result.push(obj);
  }
  return result;
}, []);

};

答案 4 :(得分:0)

如果您事先拥有这么多的信息,则可以更轻松地向后构建树。由于您对输入的形状非常了解,并且清楚地定义了它们之间的关系,因此您可以轻松地将其分成多个数组,并从下至上构建它:

import * as stuff from './stuff'
import * as stuff2 from './stuff2'
export { stuff, stuff2 }

这可能不是性能最高的解决方案,并且需要根据输入的预期形状进行一些调整,但这是一个干净易读的解决方案。

答案 5 :(得分:0)

另一种方法可能是这样的:

const countKids = (nodes) => 
  nodes.length + nodes.map(({children = []}) => countKids(children)).reduce((a, b) => a + b, 0)

const makeForest = (id, xs) => 
  xs .filter (({parentId}) => parentId == id)
     .map (({id, parentId, ...rest}) => {
       const kids = makeForest (id, xs)
       return {id, ...rest, ...(kids .length ? {count: countKids (kids), children: kids} : {})}
     })

const nest = (flat) =>
  makeForest (null, flat)
    .map ((node) => ({...node, root: true}))

const flat = [{id: "a", name: "Root 1", parentId: null}, {id: "b", name: "Root 2", parentId: null}, {id: "c", name: "Root 3", parentId: null}, {id: "a1", name: "Item 1", parentId: "a"}, {id: "a2", name: "Item 1", parentId: "a"}, {id: "b1", name: "Item 1", parentId: "b"}, {id: "b2", name: "Item 2", parentId: "b"}, {id: "b2-1", name: "Item 2-1", parentId: "b2"}, {id: "b2-2", name: "Item 2-2", parentId: "b2"}, {id: "b3", name: "Item 3", parentId: "b"}, {id: "c1", name: "Item 1", parentId: "c"}, {id: "c2", name: "Item 2", parentId: "c"}]

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

主要功能(makeForest)查找其ID与目标匹配的所有子代(最初为null),然后递归地对这些子代执行相同的操作。

这里唯一的复杂性是如果节点的子代为空,则不包括countchildren。如果包括它们不是问题,则可以简化此过程。