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

tot*_*oob 14 javascript arrays javascript-objects ecmascript-6 arrow-functions

随着阵列

['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
Run Code Online (Sandbox Code Playgroud)

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

{
    'social': {
        swipes: {
            women: null,
            men: null
        }
    },
    'upgrade': {
        premium: null
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

但我不确定如何切换价值root.

我该怎么设置root,以使之递归调用addLabelToMap与

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

我已经习惯root = root[element]但是它给出了一个错误.

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

Nit*_*ang 13

这个问题是关于创建对象并在循环遍历input数组和基于分割字符串时维护它的状态/.

这可以使用Array.reduce我们从空对象开始的地方来完成,当循环input开始我们开始填充它时,对于每个字符串中的最后一个字,我们将值赋给nullobject属性.

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)
Run Code Online (Sandbox Code Playgroud)

  • `.filter(d => d)`表示仅过滤`truthy`值 (3认同)

Jon*_*lms 6

它很简单:

 root = root[label];
Run Code Online (Sandbox Code Playgroud)

如果您将辅助函数更改为:

 const addLabelToMap = (root, label) => {
    if(!root[label]) root[label] =  {};
 }
Run Code Online (Sandbox Code Playgroud)

我把它写成:

 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;
}
Run Code Online (Sandbox Code Playgroud)


ibr*_*rir 6

用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));
Run Code Online (Sandbox Code Playgroud)

说明:

对于每一个item中menu排列,我们提取parts由第一摆脱主导的'/'(使用slice(1)),然后split通过廷'/'.

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

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

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

请注意,我们传递Object.create(null)给reduce.Object.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);
Run Code Online (Sandbox Code Playgroud)


Akr*_*ion 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)
Run Code Online (Sandbox Code Playgroud)

我们的想法是将每个路径传递给一个makeObj函数,该函数将递归地修饰一个对象,直到它到达路径数组的末尾.这是常用Array.reduce方法的另一种替代方案.


The*_*tan 5

我只是调试你的代码,看看有什么不对,我敦促你做同样的事情.你犯了两个(明显的)错误:

首先,在最初的迭代,这里的值map只是一个空的对象{},价值root被初始化为""和label的swipes.

.forEach((element) => {
  let root = map[element[0]] || "";
  ...
  root = root[label];
}
Run Code Online (Sandbox Code Playgroud)

那么你得到的root[label]是undefined新的根undefined.

其次,你map到处都在使用它.

const addLabelToMap = (root, label) => {
  if(!map[root]) map[root] = {};
  if(!map[root][label]) map[root][label] = {};
}
Run Code Online (Sandbox Code Playgroud)

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

const addLabelToMap = (root, label) => {
  if(!root[label]) root[label] = {};
}
Run Code Online (Sandbox Code Playgroud)

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


ast*_*elo 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);
Run Code Online (Sandbox Code Playgroud)

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


Nik*_*wal 5

您可以使用Array.reduce,Object.keys和String.substring 来简化代码

buildMenuMap

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

addLabelToMap

该功能需要2个输入

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

并返回更新的对象

逻辑

  • function弹出第一个value(let key = ar.shift())作为键,并在object(obj[key] = obj[key] || {};)中添加/更新.
  • 如果存在当前object(if(ar.length))的子层次结构,则递归调用该函数以更新对象直到end(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));
Run Code Online (Sandbox Code Playgroud)


Bha*_*ata 5

来自赏金描述的Citate:
当前答案中没有包含足够的细节.

我想你不明白它在目前的答案中是如何运作的.因此,我将为您提供两种解决方案:一种替代解决方案和一种具有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));
Run Code Online (Sandbox Code Playgroud)

但是如果你不理解它,那么看看这段代码:

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));
Run Code Online (Sandbox Code Playgroud)

扩展解决方案 Array.reduce()

在这个解决方案中,我使用扩展版本的用户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));
Run Code Online (Sandbox Code Playgroud)