Как рекурсивно построить объект списка меню в JavaScript? - PullRequest
0 голосов
/ 15 ноября 2018

С массивом

['/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 голосов
/ 09 декабря 2018

Вы также можете решить эту проблему с помощью рекурсивной функции в краткой форме, например:

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.

0 голосов
/ 23 ноября 2018

Эта проблема связана с созданием объекта и поддержанием его состояния во время циклического перебора массива 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)
0 голосов
/ 09 декабря 2018

Цитата из описания награды:
В текущих ответах недостаточно подробностей.

Я думаю, вы не понимаетекак это работает в текущих ответах.Поэтому я предоставлю вам два решения: одно альтернативное решение и одно расширенное решение с функцией 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()

В этом решении я использую код от пользователя 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));
0 голосов
/ 22 ноября 2018

Вы можете упростить свой код, используя Array.reduce , Object.keys & String.substring

buildMenuMap

Функция принимает массив в качестве входных данных и превращает его в объект, где для каждой записи в массиве объект обновляется с соответствующей иерархией с использованием функции addLabelToMap. Каждая запись преобразуется в массив уровней (c.substring(1).split("/")).

addLabelToMap

Функция принимает 2 входа

  • obj - текущий корневой объект / узел
  • ar - массив дочерней иерархии

и возвращает обновленный объект

Logic

  • выводит первое значение (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));
0 голосов
/ 19 ноября 2018

Попробуйте это как целостное решение:

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

0 голосов
/ 18 ноября 2018

Я просто отладил ваш код, чтобы увидеть, что не так, и я призываю вас сделать то же самое.Вы делаете две (очевидные) ошибки:

Во-первых, в самой первой итерации здесь значение map является просто пустым объектом {}, значение root инициализируется как ""и 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] = {};
}

Вместо этого вы должны принимать его как параметр, чтобы иметь возможность выполнять рекурсию.

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

Для отладки кодасоздайте простой HTML-файл с js в тегах сценария, а затем подайте его с локального компьютера, используя python -m http.server.Затем вы можете добавить точку отладки и шаг за шагом пройтись по коду.

0 голосов
/ 15 ноября 2018

Используйте 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 сначала избавившись от ведущего '/' (используя slice(1)), а затем split ting от '/'.

Затем мы удаляем 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);
0 голосов
/ 15 ноября 2018

Это так же просто, как:

 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;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...