如何在JavaScript中递归构建菜单列表对象?

时间:2018-11-14 21:20:49

标签: javascript arrays ecmascript-6 javascript-objects arrow-functions

带有

数组
['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

我想构造一个看起来像这样的地图对象:

{
    'social': {
        swipes: {
            women: null,
            men: null
        }
    },
    'upgrade': {
        premium: null
    }
}

const menu = ['/social/swipes/women', '/social/likes/men', '/upgrade/premium'];
const map = {};

const addLabelToMap = (root, label) => {
  if(!map[root]) map[root] = {};
  if(!map[root][label]) map[root][label] = {};
}

const buildMenuMap = menu => {
  menu
    // make a copy of menu
    // .slice returns a copy of the original array
    .slice()
    // convert the string to an array by splitting the /'s
    // remove the first one as it's empty
    // .map returns a new array
    .map(item => item.split('/').splice(1))
    // iterate through each array and its elements
    .forEach((element) => {
      let root = map[element[0]] || "";

      for (let i = 1; i < element.length; i++) {
        const label = element[i];
        addLabelToMap(root, label)
        // set root to [root][label]
        //root = ?
        root = root[label];
      }
    });
}

buildMenuMap(menu);

console.log(map);

但是我不确定如何切换root的值。

我将root设置为什么,以便它以递归方式调用addLabelToMap

'[social]''swipes' => '[social][swipes]''women' => '[social][swipes]''men'

我使用了root = root[element],但它给出了一个错误。

替代解决方案将是不错的选择,但我想了解为什么这根本无法奏效。

8 个答案:

答案 0 :(得分:13)

这个问题是关于创建对象并保持其状态,同时循环遍历input数组并基于/拆分字符串。

这可以使用Array.reduce来完成,其中我们以空对象开始,而循环遍历input时我们开始填充它,并为每个字符串中的最后一个单词分配null值给对象属性。

let input = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

let output = input.reduce((o, d) => {
  let keys = d.split('/').filter(d => d)
  
  keys.reduce((k, v, i) => {
    k[v] = (i != keys.length - 1)
             ? k[v] || {} 
             : null
    
    return k[v]
  }, o)
  
  return o
}, {})

console.log(output)

答案 1 :(得分:6)

这很容易:

 root = root[label];

如果您将助手功能更改为:

 const addLabelToMap = (root, label) => {
    if(!root[label]) root[label] =  {};
 }

我将其写为:

 const buildMenuMap = menus => {
   const root = {};

   for(const menu of menus) {
     const keys = menu.split("/").slice(1);
     const prop = keys.pop();
     const obj = keys.reduce((curr, key) => curr[key] || (curr[key] = {}), root);
     obj[prop] = null;
  }

  return root;
}

答案 2 :(得分:6)

使用teams = [members[i::no_of_teams] for i in range(no_of_teams)] 代替reduce。在这种情况下,根将是累加器:

map

说明:

对于const buildMenuMap = menu => menu.reduce((root, item) => { let parts = item.slice(1).split("/"); let lastPart = parts.pop(); let leaf = parts.reduce((acc, part) => acc[part] || (acc[part] = {}), root); leaf[lastPart] = null; return root; }, Object.create(null)); 数组中的每个item,我们首先去除前导menu(使用parts),然后{ {1}} '/'来结束。

然后我们从结果数组中删除slice(1)(最后一部分与其余部分分开处理)。

对于split数组中的每个其余部分,我们遍历'/'数组。在遍历的每个级别,如果对象已经存在,则返回该级别lastPart的对象,;如果不存在,则创建并返回一个新对象的对象parts

到达最后一个级别root后,我们使用acc[part]将值设置为(acc[part] = {})

请注意,我们将leaf传递给lastPartnull创建了一个没有原型的对象,因此可以更安全地使用Object.create(null),而不必检查reduce是否为拥有的财产。

示例:

Object.create(null)

答案 3 :(得分:6)

您还可以通过一种简洁的方式使用递归函数来解决此问题:

let obj={}, input = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

const makeObj = (arr, obj={}) => {
  let prop = arr.shift()
  prop in obj ? null : Object.assign(obj, {[prop]: {}})
  arr.length ? makeObj(arr, obj[prop]) : obj[prop] = null
  return obj
}

input.forEach(x => makeObj(x.split('/').filter(Boolean), obj))

console.log(obj)

这个想法是让每个路径将它们传递给makeObj函数,该函数将递归地用路径装饰对象,直到到达路径数组的末尾。这是常见的Array.reduce方法的另一种选择。

答案 4 :(得分:5)

我刚刚调试了您的代码,以查看出了什么问题,并敦促您也这样做。您犯了两个(显而易见的)错误:

首先,在第一次迭代中,map的值只是一个空对象{}root的值被初始化为""和{{ 1}}是label

swipes

因此,您得到的.forEach((element) => { let root = map[element[0]] || ""; ... root = root[label]; } root[label],因此新的根是undefined

第二,您到处都在使用undefined

map

相反,您应该将其用作参数,以便能够进行递归。

const addLabelToMap = (root, label) => {
  if(!map[root]) map[root] = {};
  if(!map[root][label]) map[root][label] = {};
}

要调试代码,请在script标签中使用js创建一个简单的HTML文件,然后使用const addLabelToMap = (root, label) => { if(!root[label]) root[label] = {}; } 从本地计算机上投放该文件。然后,您可以添加调试点并逐步执行代码。

答案 5 :(得分:5)

尝试将其作为整体解决方案:

const menu = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

const deepMerge = (target, source) => {
  // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
  for (let key of Object.keys(source)) {
    if (source[key] instanceof Object && key in target) Object.assign(source[key], deepMerge(target[key], source[key]))
  }

  // Join `target` and modified `source`
  Object.assign(target || {}, source)
  return target
};

const buildMenuMap = menu => {
  return menu
    .map(item => item.split('/').splice(1))

    // The `root` value is the object that we will be merging all directories into
    .reduce((root, directory) => {

      // Iterates backwards through each directory array, stacking the previous accumulated object into the current one
      const branch = directory.slice().reverse().reduce((acc, cur) => { const obj = {}; obj[cur] = acc; return obj;},null);

      // Uses the `deepMerge()` method to stitch together the accumulated `root` object with the newly constructed `branch` object.
      return deepMerge(root, branch);
    }, {});
};

buildMenuMap(menu);

注意:深度合并解决方案取自@ahtcx上的GitHubGist

答案 6 :(得分:5)

您可以使用Array.reduceObject.keysString.substring

简化代码

buildMenuMap

该函数将数组作为输入并将其简化为一个对象,其中对于数组中的每个条目,使用addLabelToMap函数以相应的层次结构更新该对象。每个条目都转换为一个级别数组(c.substring(1).split("/"))。

addLabelToMap

该功能需要2个输入

  • obj -当前的根对象/节点
  • ar -子层次结构数组

返回更新后的对象

逻辑

  • 函数弹出第一个值(let key = ar.shift())作为键,并在对象(obj[key] = obj[key] || {};)中添加/更新。
  • 如果当前对象(if(ar.length))具有子层次结构,则递归调用该函数以更新对象直至结尾(addLabelToMap(obj[key], ar))。
  • 否则(没有其他子层次结构),检查对象是否由于数组中的其他条目而具有某些层次结构(else if(!Object.keys(obj[key]).length))。如果没有层次结构,即它是叶子,则将值设置为nullobj[key] = null)。 注意,如果永远不会出现像/social/swipes/men/young这样的数组中存在与现有条目相同的条目,则else if块可以简化为简单的else阻止。
  • 对象已更新,返回最终更新的对象

let arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

function addLabelToMap(obj, ar) {
  let key = ar.shift();
  obj[key] = obj[key] || {}; 
  if(ar.length) addLabelToMap(obj[key], ar);
  else if(!Object.keys(obj[key]).length) obj[key] = null;
  return obj;
}

function buildMenuMap(ar) {
  return ar.reduce((a,c) => addLabelToMap(a,c.substring(1).split("/")), {});
}

console.log(buildMenuMap(arr));

答案 7 :(得分:5)

  

引用赏金描述:
  当前答案没有足够的细节。

我认为您不了解当前答案中的工作原理。因此,我将为您提供两种解决方案:一种替代解决方案和一种具有 Array.reduce() 功能的扩展解决方案。

带有for循环的替代解决方案

有关代码的说明,请参见代码注释。

var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'],
    result = {};

//if you want to have it shorter you can write for(var i = arr.length; i--;) too
for(var i = 0; i < arr.length; i++)
{
    var parts = arr[i].slice(1).split('/'),
        //if we have already one object then we take it. If not the we create new one:
        curObj = result[parts[0]] = result[parts[0]] || {};

    for(var k = 1; k < parts.length; k++)
    {
        //if we have next part
        if(parts[k+1])
            //if we have already one object then we take it. If not the we create new one:
            curObj[parts[k]] = curObj[parts[k]] || {};
        //if we do not have next part
        else curObj[parts[k]] = null;
        //or if-else block in one line:
        //curObj[parts[k]] = parts[k+1] ? (curObj[parts[k]] || {}) : null;

        //link to next object:
        curObj = curObj[parts[k]];
    }
}

console.log(JSON.stringify(result, null, 4));

但是,如果您不理解它,请参阅以下代码:

var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'],
    result = {},
    parts = arr[2].slice(1).split('/'),
    curObj = result[parts[0]] = {};

curObj[parts[1]] = parts[1+1] ? {} : null;
console.log(JSON.stringify(result, null, 4));

使用Array.reduce()

扩展了解决方案

在此解决方案中,我使用扩展版本的 user Nitish Narang 中的代码,并在注释和控制台输出中进行了一些解释-这样,您就可以在控制台中看到代码的作用。我的建议:如果您不理解带有箭头功能的代码,请使用正常的功能和适当的变量名完整地编写代码,以说明自己。我们(人类)需要一些图片来想象所有事物。如果我们只有一些简短的变量名,那么很难想象并理解所有这些。我还有一点“缩短了”他的代码。

var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
var result = arr.reduce(function(acc0, curVal, curIdx)
{
    console.log('\n' + curIdx + ': ------------\n'
                + JSON.stringify(acc0, null, 4));

    var keys = curVal.slice(1).split('/');
    keys.reduce(function(acc1, currentValue, currentIndex)
    {
        acc1[currentValue] = keys[currentIndex+1]
                                ? acc1[currentValue] || {}
                                : null;

        return acc1[currentValue]
    }, acc0); //acc0 - initialValue is the same object, but it is empty only in first cycle

    return acc0
}, {}); // {} - initialValue is empty object


console.log('\n------result------\n'
            + JSON.stringify(result, null, 4));