根据字符串属性将对象数组转换为嵌套的对象数组?

时间:2015-06-25 13:38:22

标签: javascript arrays tree javascript-objects lodash

我遇到了一个问题,试图将平面对象数组转换为基于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等库来实现所需的输出。

非常感谢任何帮助。

5 个答案:

答案 0 :(得分: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, []);

它通过重新整形初始输入来工作,以便将名称拆分为数组并添加data / children属性。然后它减少buildTree上的数据,该数据使用变异函数(:()在给定路径的reduce中插入当前项。

奇怪的if (!match)部分确保在未使用URL等在初始数据集中明确指定缺少的段时添加它们。

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

应该很简单,但是可以遵循。

答案 1 :(得分:0)

此解决方案仅使用本机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;
}

答案 2 :(得分:0)

这是我基于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. 然后它必须处理这样一个事实:对于长链,hello中的foo.hello.world之类的中间节点没有任何数据,所以它必须插入data: {}name属性。
  3. 最后,它消除了剩下的东西:把标题放在一边。 data属性中的字幕,并清理仍然完全合格的name个。
  4. 代码:

    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"
        }
    ]
    

    胜利者去战利品。

答案 3 :(得分:0)

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

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

defer teardown()

答案 4 :(得分:0)

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

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

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

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

接下来,在每个元素上应用两个转换:(1)将标题和副标题清理为子属性data和(2),这有点棘手,找到所有中间路径,如{{ 1}}和buzz并未在foo.hello中表示但其子女为。展平此数组数组,并按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); 的平面数组加上所有中间路径。在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.0buzz
  • children.1foo.bar

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

代码:

partial

此对象/ hash 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表路径:它使用单个键_.set初始化,并且迭代添加到它。在运行此代码后,查看此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

嵌入式代码段只包含没有中间JSON的代码,可以分散您的注意力。

这比我之前的提交更好吗?我是这么认为的:这里的簿记要少得多,不透明,繁忙的代码和更高层次的构造。我认为缺乏递归会有所帮助。我认为最终[ { "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" } } ] } ] 可能会替换为forEach,但我没有尝试过,因为算法的其余部分是基于矢量和迭代的,我不想要与此分道扬。

很遗憾在ES6中留下了所有内容,我非常喜欢它:)

&#13;
&#13;
reduce
&#13;
&#13;
&#13;