【问题标题】:Convert an array of objects into a nested array of objects based on string property?将对象数组转换为基于字符串属性的嵌套对象数组?
【发布时间】:2023-03-12 05:03:01
【问题描述】:

我在尝试将平面对象数组转换为基于 name 属性的嵌套对象数组时遇到问题。

input 数组转换为类似于desiredOutput 数组结构的最佳方法是什么?

var input = [
    { 
        name: 'foo', 
        url: '/somewhere1',
        templateUrl: 'foo.tpl.html',
        title: 'title A', 
        subtitle: 'description A' 
    },
    { 
        name: 'foo.bar', 
        url: '/somewhere2', 
        templateUrl: 'anotherpage.tpl.html', 
        title: 'title B', 
        subtitle: 'description B' 
    },
    { 
        name: 'buzz.fizz',
        url: '/another/place',
        templateUrl: 'hello.tpl.html',  
        title: 'title C',  
        subtitle: 'description C' 
    },
    { 
        name: 'foo.hello.world', 
        url: '/',
        templateUrl: 'world.tpl.html',
        title: 'title D',   
        subtitle: 'description D' 
    }
]

var desiredOutput = [
    {
        name: 'foo',
        url: '/somewhere1',
        templateUrl: 'foo.tpl.html',
        data: {
            title: 'title A',
            subtitle: 'description A'
        },
        children: [
            {
                name: 'bar',
                url: '/somewhere2', 
                templateUrl: 'anotherpage.tpl.html',
                data: {
                    title: 'title B', 
                    subtitle: 'description B'
                }
            },
            {
                name: 'hello',
                data: {},
                children: [
                    {
                        name: 'world',
                        url: '/',
                        templateUrl: 'world.tpl.html',
                        data: {
                            title: 'title D',   
                            subtitle: 'description D'
                        }
                    }
                ]
            }
        ]
    },
    {
        name: 'buzz',
        data: {},
        children: [
            {
                name: 'fizz',
                url: '/',
                templateUrl: 'world.tpl.html',
                data: {
                    title: 'title C',   
                    subtitle: 'description C'
                }
            }
        ]
    }
]

请注意,输入数组中对象的顺序无法保证。 此代码将在 Node.js 环境中运行,我愿意使用 lodash 等库来实现所需的输出。

非常感谢任何帮助。

【问题讨论】:

  • 请在您的问题中添加您的代码
  • 我的代码不能比问题长,所以 Stackoverflow 不允许我这样做。因此粘贴箱链接。我希望数据集足够详细,以充分表达问题。
  • 所以父子关系是基于input[].name拆分.?或者你能进一步确定这种关系吗?
  • @matt_d_rat 你当然可以,而且你需要!在您的问题本身中包含代码。不是每个人都可以访问外部网站,并且链接可能会随着时间的推移而中断。
  • @Magicprog.fr 好吧,这很奇怪,当我编辑它时它让我这样做了。我第一次发帖时没有。很奇怪。

标签: javascript arrays tree javascript-objects lodash


【解决方案1】:

使用 Lodash(因为你到底为什么要在没有实用程序库的情况下操作复杂的数据)。这里是the fiddle

function formatRoute(route) {
    return _.merge(_.pick(route, ['url', 'templateUrl']), {
        name: route.name.split('.'),
        data: _.pick(route, ['title', 'subtitle']),
        children: []
    });
}

function getNameLength(route) {
    return route.name.length;
}

function buildTree(tree, route) {
    var path = _.slice(route.name, 0, -1);

    insertAtPath(tree, path, _.merge({}, route, {
        name: _.last(route.name)
    }));

    return tree;
}

function insertAtPath(children, path, route) {
    var head = _.first(path);

    var match = _.find(children, function (child) {
        return child.name === head;
    });

    if (path.length === 0) {
        children.push(route);
    }
    else {
        if (!match) {
            match = {
                name: head,
                data: {},
                children: []
            };
            children.push(match);
        }

        insertAtPath(match.children, _.rest(path), route);
    }
}


// Map the routes into their correct formats.
var routes = _.sortBy(_.map(input, formatRoute), getNameLength);

// Now we can reduce this well formatted array into the desired format.
var out = _.reduce(routes, buildTree, []);

它的工作原理是重塑初始输入,以便将名称拆分为数组并添加数据/子属性。然后它减少了buildTree 上的数据,它使用一个变异函数(:()将当前项插入到给定路径的 reduce 中。

奇怪的if (!match) 部分确保如果缺少的段没有在初始数据集中使用 URL 等明确指定,则会添加它们。

实际完成工作的最后两行应该是在一个小函数中,它可以用一些 JSDoc 来完成。可惜我没有完全递归,我依靠数组突变将路由对象插入到树的深处。

不过应该​​足够简单。

【讨论】:

  • 我喜欢你将工作分成不同的函数,使代码易于理解。我将与其他发布的解决方案一起尝试您的解决方案,并投票选出最适合我需求的解决方案。感谢您的贡献。
  • 我接受了你的回答@Olical,因为对我来说这是最容易理解的,它解决了问题。所有其他提交的内容也都有效,而且同样出色。
  • @matt_d_rat 很高兴我能帮上忙 :)
【解决方案2】:

这是我基于 Lodash 的尝试。

首先,我发现_.set可以理解深度嵌套的对象表示法,所以我用它来构建一个编码父子关系的树:

var tree = {};
input.forEach(o => _.set(tree, o.name, o));

这会产生:

{
    "foo": {
        "name": "foo",
        "url": "/somewhere1",
        "templateUrl": "foo.tpl.html",
        "title": "title A",
        "subtitle": "description A",
        "bar": {
            "name": "foo.bar",
            "url": "/somewhere2",
            "templateUrl": "anotherpage.tpl.html",
            "title": "title B",
            "subtitle": "description B"
        },
        "hello": {
            "world": {
                "name": "foo.hello.world",
                "url": "/",
                "templateUrl": "world.tpl.html",
                "title": "title D",
                "subtitle": "description D"
            }
        }
    },
    "buzz": {
        "fizz": {
            "name": "buzz.fizz",
            "url": "/another/place",
            "templateUrl": "hello.tpl.html",
            "title": "title C",
            "subtitle": "description C"
        }
    }
}

这实际上与期望的输出相去甚远。但是孩子的名字显示为属性,以及其他属性,如title

然后是编写一个递归函数的艰苦过程,该函数采用这个中间树并以您希望的方式重新格式化它:

  1. 首先需要找到子属性,并将它们移动到children属性数组中。
  2. 然后它必须处理这样一个事实,对于长链,foo.hello.world中的hello这样的中间节点没有任何数据,因此它必须插入data: {}name属性。
  3. 最后,它清理了剩下的内容:将标题和副标题放在 data 属性中,并清理所有仍然完全合格的 names。

代码:

var buildChildrenRecursively = function(tree) {
  var children = _.keys(tree).filter(k => _.isObject(tree[k]));
  if (children.length > 0) {

    // Step 1 of reformatting: move children to children
    var newtree = _.omit(tree, children);
    newtree.children = children.map(k => buildChildrenRecursively(tree[k]));

    // Step 2 of reformatting: deal with long chains with missing intermediates
    children.forEach((k, i) => {
      if (_.keys(newtree.children[i]).length === 1) {
        newtree.children[i].data = {};
        newtree.children[i].name = k;
      }
    });

    // Step 3 of reformatting: move title/subtitle to data; keep last field in name
    newtree.children = newtree.children.map(function(obj) {
      if ('data' in obj) {
        return obj;
      }
      var newobj = _.omit(obj, 'title,subtitle'.split(','));
      newobj.data = _.pick(obj, 'title,subtitle'.split(','));
      newobj.name = _.last(obj.name.split('.'));
      return newobj;
    });

    return (newtree);
  }
  return tree;
};

var result = buildChildrenRecursively(tree).children;

输出:

[
    {
        "name": "foo",
        "url": "/somewhere1",
        "templateUrl": "foo.tpl.html",
        "children": [
            {
                "name": "bar",
                "url": "/somewhere2",
                "templateUrl": "anotherpage.tpl.html",
                "data": {
                    "title": "title B",
                    "subtitle": "description B"
                }
            },
            {
                "children": [
                    {
                        "name": "world",
                        "url": "/",
                        "templateUrl": "world.tpl.html",
                        "data": {
                            "title": "title D",
                            "subtitle": "description D"
                        }
                    }
                ],
                "data": {},
                "name": "hello"
            }
        ],
        "data": {
            "title": "title A",
            "subtitle": "description A"
        }
    },
    {
        "children": [
            {
                "name": "fizz",
                "url": "/another/place",
                "templateUrl": "hello.tpl.html",
                "data": {
                    "title": "title C",
                    "subtitle": "description C"
                }
            }
        ],
        "data": {},
        "name": "buzz"
    }
]

胜利者归战利品。

【讨论】:

  • 哇,您使用 _.set 方法确实让您非常痛苦。我将尝试您的方法以及发布的其他解决方案,并投票选出我认为最适合我需要的解决方案。就我个人而言,我不是 lambda 表达式的粉丝,目前我对 ES6 相对较新,我发现它们让我在阅读代码时思考的时间比必要的微秒长。但这只是个人喜好而不是批评。感谢您的贡献。
  • 我认为_.set 可以让我们比我之前想象的更接近……也许吧!
  • 是的!使用_.set superpowers,我提交了第二个答案:P 请参阅stackoverflow.com/a/31064609/500207
【解决方案3】:

此解决方案仅使用原生 JS 方法。它肯定可以优化,但我保留它是为了更容易理解(或者我希望如此)。我还注意不要修改原始输入,因为 JS 通过引用传递对象。

var input = [{
  name: 'foo',
  url: '/somewhere1',
  templateUrl: 'foo.tpl.html',
  title: 'title A',
  subtitle: 'description A'
}, {
  name: 'foo.bar',
  url: '/somewhere2',
  templateUrl: 'anotherpage.tpl.html',
  title: 'title B',
  subtitle: 'description B'
}, {
  name: 'buzz.fizz',
  url: '/another/place',
  templateUrl: 'hello.tpl.html',
  title: 'title C',
  subtitle: 'description C'
}, {
  name: 'foo.hello.world',
  url: '/',
  templateUrl: 'world.tpl.html',
  title: 'title D',
  subtitle: 'description D'
}];

// Iterate over input array elements
var desiredOutput = input.reduce(function createOuput(arr, obj) {
  var names = obj.name.split('.');
  // Copy input element object as not to modify original input
  var newObj = Object.keys(obj).filter(function skipName(key) {
    return key !== 'name';
  }).reduce(function copyObject(tempObj, key) {
    if (key.match(/url$/i)) {
      tempObj[key] = obj[key];
    }
    else {
      tempObj.data[key] = obj[key];
    }

    return tempObj;
  }, {name: names[names.length - 1], data: {}});

  // Build new output array with possible recursion
  buildArray(arr, names, newObj);

  return arr;
}, []);

document.write('<pre>' + JSON.stringify(desiredOutput, null, 4) + '</pre>');

// Helper function to search array element objects by name property
function findIndexByName(arr, name) {
  for (var i = 0, len = arr.length; i < len; i++) {
    if (arr[i].name === name) {
      return i;
    }
  }

  return -1;
}

// Recursive function that builds output array
function buildArray(arr, paths, obj) {
  var path = paths.shift();
  var index = findIndexByName(arr, path);

  if (paths.length) {
    if (index === -1) {
      arr.push({
        name: path,
        children: []
      });

      index = arr.length - 1;
    }

    if (!Array.isArray(arr[index].children)) {
      arr[index].children = [];
    }

    buildArray(arr[index].children, paths, obj);
  } else {
    arr.push(obj);
  }

  return arr;
}

【讨论】:

  • 总是很高兴看到纯 JS 实现以及比较。我将与其他人一起发布您的解决方案,并根据最适合我的要求进行投票。感谢您的贡献。
【解决方案4】:

此解决方案不使用递归,它使用指向对象图中前一项的引用指针。

请注意,此解决方案确实使用了 lodash。 JSFiddle 示例在这里http://jsfiddle.net/xpb75dsn/1/

var input = [
    {
        name: 'foo',
        url: '/somewhere1',
        templateUrl: 'foo.tpl.html',
        title: 'title A',
        subtitle: 'description A'
    },
    {
        name: 'foo.bar',
        url: '/somewhere2',
        templateUrl: 'anotherpage.tpl.html',
        title: 'title B',
        subtitle: 'description B'
    },
    {
        name: 'buzz.fizz',
        url: '/another/place',
        templateUrl: 'hello.tpl.html',
        title: 'title C',
        subtitle: 'description C'
    },
    {
        name: 'foo.hello.world',
        url: '/',
        templateUrl: 'world.tpl.html',
        title: 'title D',
        subtitle: 'description D'
    }
];

var nameList = _.sortBy(_.pluck(input, 'name'));
var structure = {};

var mapNav = function(name, navItem) {
    return {
        name : name,
        url : navItem.url,
        templateUrl : navItem.templateUrl,
        data : { title : navItem.title, subtitle : navItem.subtitle },
        children : []
    };
};

_.map(nameList, function(fullPath) {
    var path = fullPath.split('.');
    var parentItem = {};
    _.forEach(path, function(subName, index) {
        var navItem = _.find(input, { name : fullPath });
        var item = mapNav(subName, navItem);
        if (index == 0) {
            structure[subName] = item;
        } else {
            parentItem.children.push(item);
        }
        parentItem = item;
    });
});


var finalStructure = Object.keys(structure).map(function(key) {
    return structure[key];
});

console.log(finalStructure);  

【讨论】:

    【解决方案5】:

    这是一个使用 lodash 的完全无递归的方法。当我想到_.set_.get 有多好时,我突然想到,我意识到我可以用children 的序列替换对象“路径”。

    首先,构建一个对象/哈希表,其键等于input 数组的name 属性:

    var names = _.object(_.pluck(input, 'name'));
    // { foo: undefined, foo.bar: undefined, buzz.fizz: undefined, foo.hello.world: undefined }
    

    (不要尝试JSON.stringify这个对象!因为它的值都是未定义的,它的计算结果是{}...)

    接下来,对每个元素应用两个转换:(1) 将标题和副标题清理为子属性 data,以及 (2) 这有点棘手,找到所有中间路径,如 buzzfoo.hello 未在 input 中表示,但其孩子在。展平这个array-of-arrays并按name字段中.的数量对它们进行排序。

    var partial = _.flatten(
        input.map(o =>
                  {
                    var newobj = _.omit(o, 'title,subtitle'.split(','));
                    newobj.data = _.pick(o, 'title,subtitle'.split(','));
                    return newobj;
                  })
            .map(o => {
              var parents = o.name.split('.').slice(0, -1);
              var missing =
                  parents.map((val, idx) => parents.slice(0, idx + 1).join('.'))
                      .filter(name => !(name in names))
                      .map(name => {
                        return {
                          name,
                          data : {},
                        }
                      });
    
              return missing.concat(o);
            }));
    partial = _.sortBy(partial, o => o.name.split('.').length);
    

    这段代码可能看起来很吓人,但看看它的输出应该会让你相信它非常简单:它只是一个包含原始 input 以及所有不在 input 中的中间路径的平面数组,按点数排序在name 中,每个都有一个新的data 字段。

    [
        {
            "name": "foo",
            "url": "/somewhere1",
            "templateUrl": "foo.tpl.html",
            "data": {
                "title": "title A",
                "subtitle": "description A"
            }
        },
        {
            "name": "buzz",
            "data": {}
        },
        {
            "name": "foo.bar",
            "url": "/somewhere2",
            "templateUrl": "anotherpage.tpl.html",
            "data": {
                "title": "title B",
                "subtitle": "description B"
            }
        },
        {
            "name": "buzz.fizz",
            "url": "/another/place",
            "templateUrl": "hello.tpl.html",
            "data": {
                "title": "title C",
                "subtitle": "description C"
            }
        },
        {
            "name": "foo.hello",
            "data": {}
        },
        {
            "name": "foo.hello.world",
            "url": "/",
            "templateUrl": "world.tpl.html",
            "data": {
                "title": "title D",
                "subtitle": "description D"
            }
        }
    ]
    

    我们快到家了。最后剩下的一点魔法需要存储一些全局状态。我们将遍历这个平面 partial 数组,将 name 字段替换为 _.get_.set 可以使用包含 children 和数字索引的路径:

    • foo 映射到 children.0
    • buzzchildren.1,
    • foo.barchildren.0.children.0 等。

    当我们迭代(不是递归!)构建这个路径序列时,我们使用_.set 将上面partial 的每个元素注入到适当的位置。

    代码:

    var name2path = {'empty' : ''};
    var out = {};
    partial.forEach(obj => {
      var split = obj.name.split('.');
      var par = name2path[split.slice(0, -1).join('.') || "empty"];
      var path = par + 'children.' + (_.get(out, par + 'children') || []).length;
      name2path[obj.name] = path + '.';
      _.set(out, path, obj);
    });
    out = out.children;
    

    此对象/哈希name2path 将名称转换为_.settable 路径:它使用单个键empty 进行初始化,并且迭代添加到它。运行这段代码后,看看name2path 是什么会很有帮助:

    {
        "empty": "",
        "foo": "children.0.",
        "buzz": "children.1.",
        "foo.bar": "children.0.children.0.",
        "buzz.fizz": "children.1.children.0.",
        "foo.hello": "children.0.children.1.",
        "foo.hello.world": "children.0.children.1.children.0."
    }
    

    注意迭代如何增加索引以在children 属性数组中存储多个条目。

    最终生成的out

    [
        {
            "name": "foo",
            "url": "/somewhere1",
            "templateUrl": "foo.tpl.html",
            "data": {
                "title": "title A",
                "subtitle": "description A"
            },
            "children": [
                {
                    "name": "foo.bar",
                    "url": "/somewhere2",
                    "templateUrl": "anotherpage.tpl.html",
                    "data": {
                        "title": "title B",
                        "subtitle": "description B"
                    }
                },
                {
                    "name": "foo.hello",
                    "data": {},
                    "children": [
                        {
                            "name": "foo.hello.world",
                            "url": "/",
                            "templateUrl": "world.tpl.html",
                            "data": {
                                "title": "title D",
                                "subtitle": "description D"
                            }
                        }
                    ]
                }
            ]
        },
        {
            "name": "buzz",
            "data": {},
            "children": [
                {
                    "name": "buzz.fizz",
                    "url": "/another/place",
                    "templateUrl": "hello.tpl.html",
                    "data": {
                        "title": "title C",
                        "subtitle": "description C"
                    }
                }
            ]
        }
    ]
    

    嵌入的 sn-p 仅包含代码,没有中间 JSON 来分散您的注意力。

    这比我之前的提交更好吗?我认为是这样:这里的簿记少了很多,不透明的“繁忙代码”少了,高级构造多了。我认为缺乏递归会有所帮助。我认为最终的 forEach 可能会被替换为 reduce,但我没有尝试这样做,因为算法的其余部分是如此基于向量和迭代的,我不想偏离这一点。

    很抱歉把所有东西都留在了 ES6 中,我非常喜欢它 :)

    var input = [{
      name: 'foo',
      url: '/somewhere1',
      templateUrl: 'foo.tpl.html',
      title: 'title A',
      subtitle: 'description A'
    }, {
      name: 'foo.bar',
      url: '/somewhere2',
      templateUrl: 'anotherpage.tpl.html',
      title: 'title B',
      subtitle: 'description B'
    }, {
      name: 'buzz.fizz',
      url: '/another/place',
      templateUrl: 'hello.tpl.html',
      title: 'title C',
      subtitle: 'description C'
    }, {
      name: 'foo.hello.world',
      url: '/',
      templateUrl: 'world.tpl.html',
      title: 'title D',
      subtitle: 'description D'
    }];
    
    var names = _.object(_.pluck(input, 'name'));
    
    var partial = _.flatten(
      input.map(o => {
        var newobj = _.omit(o, 'title,subtitle'.split(','));
        newobj.data = _.pick(o, 'title,subtitle'.split(','));
        return newobj;
      })
      .map(o => {
        var parents = o.name.split('.').slice(0, -1);
        var missing =
          parents.map((val, idx) => parents.slice(0, idx + 1).join('.'))
          .filter(name => !(name in names))
          .map(name => {
            return {
              name,
              data: {},
            }
          });
    
        return missing.concat(o);
      }));
    partial = _.sortBy(partial, o => o.name.split('.').length);
    
    var name2path = {
      'empty': ''
    };
    var out = {};
    
    partial.forEach(obj => {
      var split = obj.name.split('.');
      var par = name2path[split.slice(0, -1).join('.') || "empty"];
      var path = par + 'children.' + (_.get(out, par + 'children') || []).length;
      name2path[obj.name] = path + '.';
      _.set(out, path, obj);
    });
    out = out.children;

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-03-23
      • 1970-01-01
      • 1970-01-01
      • 2019-04-26
      • 2019-03-04
      • 1970-01-01
      • 2020-01-15
      • 2018-01-04
      相关资源
      最近更新 更多