【问题标题】:How to build a menu list object recursively in JavaScript?如何在 JavaScript 中递归地构建菜单列表对象?
【发布时间】:2019-04-17 22:23:33
【问题描述】:

有一个数组

['/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' =&gt; '[social][swipes]', 'women' =&gt; '[social][swipes]', 'men'?

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

替代解决方案会很棒,但我想了解为什么这从根本上不起作用。

【问题讨论】:

  • 不应该menlikes 对象而不是swipes 对象中?
  • 我刚刚编辑了它

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


【解决方案1】:

这个问题是关于创建对象并在循环通过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)

【讨论】:

  • let keys = d.split('/').filter(d => d) 怎么去掉空条目?
  • .filter(d =&gt; d) 仅代表过滤器 truthy
  • @totalnoob 当然。在那里添加了答案
【解决方案2】:

这很简单:

 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;
}

【讨论】:

  • 太棒了。你能告诉我为什么即使正确设置root,当前的代码也不起作用?
  • @totalnoob 因为addLabelToMap 没有深入地图
  • 当我使用 root = root[label] 时,如果我打印出来,在下一个循环中 root 是未定义的
【解决方案3】:

使用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));

说明:

对于menu 数组中的每个item,我们首先通过删除前导'/'(使用slice(1))然后将splitting 删除'/' 来提取parts

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

对于parts 数组中的每个剩余部分,我们遍历root 数组。在遍历的每一层,我们要么返回该层的对象acc[part],如果它已经存在,或者如果它不存在,我们创建并返回一个新的(acc[part] = {})

到最后一级leaf后,我们使用lastPart将值设置为null

请注意,我们将Object.create(null) 传递给reduceObject.create(null) 创建了一个无原型对象,因此使用 root[someKey] 会更安全,而无需检查 someKey 是否是自有属性。

示例:

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));

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

let result = buildMenuMap(arr);

console.log(result);

【讨论】:

  • 我喜欢这个。谢谢。你能解释一下为什么原始代码不起作用或者我该如何修复它?
【解决方案4】:

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

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

.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] = {};
}

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

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

要调试您的代码,请使用脚本标签中的 js 创建一个简单的 HTML 文件,然后使用 python -m http.server 从您的本地计算机提供它。然后,您可以添加一个调试点并逐步检查您的代码。

【讨论】:

    【解决方案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 on GitHubGist

    【讨论】:

      【解决方案6】:

      您可以使用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)),因为数组中有其他条目。如果没有层次结构,即它是叶子,则将值设置为null (obj[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】:

        你也可以用递归函数以简洁的方式解决这个问题:

        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 方法的另一种替代方法。

        【讨论】:

        • 我喜欢这个。我刚开始使用 Array.reduce,所以其他解决方案并不那么容易理解。
        【解决方案8】:

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

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

        for 循环的替代解决方案

        代码解释见代码cmets。

        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 的代码,并在 cmets 和控制台输出中进行了一些解释——因此你可以在控制台中看到代码的作用。我的建议:如果你不理解带有箭头函数的代码,那么用普通函数和适当的变量名来完整地编写它,这些变量名可以解释自己。我们(人类)需要一些图片来想象所有事物。如果我们只有一些简短的变量名,那么很难想象和理解这一切。我还稍微“缩短”了他的代码。

        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));

        【讨论】:

        • curObj = result[parts[0]] = result[parts[0]] || {};做什么?
        • @totalnoob,如果我们在parts[0] 中有值upgrade(也可能是social),那么我们问:是result["upgrade"] != "undefined"如果是 i>,那么我们取相同的值:result["upgrade"] = result["upgrade"]。但是如果不是那么我们创建一个新对象:result["upgrade"] = {}。同样的值得到我们的新变量curObj。在稍微扩展的版本中,我们可以这样写:if(result[parts[0]] != "undefined") result[parts[0]] = result[parts[0]]; else result[parts[0]] = {}; var curObj = result[parts[0]];我希望你现在能理解了吗?
        猜你喜欢
        • 1970-01-01
        • 2019-09-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-25
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多