Как использовать рекурсию для сглаживания объекта JavaScript

Пошаговое руководство по проблеме рекурсивного алгоритма.

Эта проблема:

Given an object oldObj, write a function flattenObject that returns a flattened version of it. 
If a certain key is empty, it should be excluded from the output. 
When you concatenate keys, make sure to add the dot character between them. For instance when flattening KeyB, c and d the result key would be KeyB.c and KeyB.d. 
Example: 
const oldObject = {
    "KeyA": 1, 
    "KeyB":{
        "c": 2, 
        "d": 3, 
        "e":{
            "f": 7, 
            "" : 2
         }
      }
}
Output: 
{
    "KeyA": 1, 
    "KeyB.c": 2, 
    "KeyB.d": 3, 
    "KeyB.e.f": 7, 
    "KeyB.e": 2
}

Шаг 1. Разберитесь в проблеме

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

Шаг 2: определитесь с подходом

Чтобы решить эту проблему, мы переберем все пары ключ / значение в нашем объекте, и если эти значения также являются объектом, мы будем перебирать пары ключ / значение в этом объекте до тех пор, пока мы не извлечем все наши индивидуальные значения и не назначим их к соответствующему ключу. Мы не знаем, насколько глубоко это вложение будет продолжаться, поэтому эта проблема является хорошим кандидатом для рекурсии.

Мы можем использовать наш рекурсивный вызов для циклического перебора каждой записи в объекте, который имеет значение, которое также является объектом, до тех пор, пока все наши объекты не будут исключены из вложений. Нам также нужно будет выполнить некоторые проверки по мере продвижения, чтобы увидеть, являются ли наши ключи пустыми строками, и построить наши новые имена ключей из вложенных имен ключей.

Шаг 3. Кодируйте свое решение

Начнем с определения нового объекта, который мы собираемся вернуть в конце нашей функции:

function flattenObject(oldObject){
    let newObject = {}
}

Мы собираемся написать вспомогательную функцию, которая будет выполнять нашу рекурсивную логику для этой функции. Это позволяет нам определять newObject вне области видимости нашего рекурсивного вызова и закрывать его значение без повторного определения его как пустого массива каждый раз, когда мы вызываем функцию.

function flattenObject(oldObject){
    let newObject = {}
    
    flattenHelper(oldObject, newObject, "")
}

Мы собираемся реализовать всю нашу логику в нашей функции flatten, которую мы определим через мгновение, но пока мы знаем, что в конце всего этого мы хотим вернуть наш newObject, поэтому сейчас мы можем закончить нашу функцию flattenObject вернув newObject.

function flattenObject(oldObject){
    let newObject = {}
    
    flattenHelper(oldObject, newObject, "")
    return newObject
}

Теперь мы определим нашу функцию flattenHelper, которая будет содержать нашу рекурсивную логику. Эта функция будет принимать в качестве параметра то, что я называю нашим currentObject, который будет каждым новым вложенным значением, когда мы продвигаемся вниз по значениям нашего объекта. Также потребуется наш newObject, который мы создаем, а также то, что я называю нашим previousKeyName. На каждом уровне мы будем отслеживать последнее имя ключа, которое мы видели перед вложенным объектом. По мере продвижения по уровням мы будем добавлять каждое новое имя ключа к этому аргументу, чтобы мы могли передавать его на более низкие уровни нашего объекта при выполнении наших рекурсивных вызовов. Пояснение ниже:

function flattenHelper(currentObject, newObject, previousKeyName) {
    for (let key in currentObject) {
        let value = currentObject[key];
    
    if (value.constructor !== Object) {
        if (previousKeyName == null || previousKeyName == '') {
            newObject[key] = value;
        } else {
            if(key == null || key == '') {
                 newObject[previousKeyName] = value;
            }else{
                newObject[previousKeyName + '.' + key] = value;
            }
    } else {
            if (previousKeyName == null || previousKeyName == '') {             
                flattenHelper(value, newObject, key);
            } else {
       flattenHelper(value, newObject, previousKeyName + '.' + key);
            }
       }
    }
}
  1. Мы перебираем наш объект и для каждого ключа присваиваем значение ключа переменной value.
  2. Мы проверяем, является ли наше значение вложенным объектом. Я делаю это, проверяя, является ли конструктор для нашего значения Object. Если бы я просто использовал typeof, который также вернул бы true, если бы это был массив, таким образом я уверен, что проверяю только объектные литералы. Если это не объект, то мы должны проверить, было ли наше previousKeyName нулевым или пустой строкой. Если это так, то мы присваиваем значение нашему текущему ключу в нашем новом объекте. Фактически просто переместите его на новый объект, ничего не меняя.
  3. В противном случае мы проверяем, является ли наш текущий ключ нулевым или равным пустой строке, если это так, мы используем previousKeyName вместо имени нашего ключа.
  4. Если наш ключ не является нулем или пустой строкой и у нашего предыдущего ключа было имя, мы добавляем его к нашему текущему ключу с точкой между ними.
  5. Если наше текущее значение является вложенным объектом, мы также проверяем, был ли наш предыдущий ключ нулевым или пустой строкой. Если бы это было так, мы просто передаем наш текущий ключ, чтобы он был предыдущимKeyName для нашего следующего вызова функции, и вызываем нашу функцию рекурсивно, чтобы проверить следующий уровень ниже.
  6. В противном случае мы вызываем нашу функцию рекурсивно, чтобы проверить следующий уровень вниз, и передаем предыдущее имя ключа, добавленное к нашему текущему имени ключа, чтобы оно было именем предыдущего ключа для следующего вызова нашей функции.

Полный код ниже:

Если вы не уверены, как работает рекурсия в этой функции, я действительно рекомендую использовать такой сайт, как AlgoViz.io, чтобы вы могли физически увидеть, как вызовы функций добавляются в стек вызовов. Если у вас есть вопросы или вы просто хотите подключиться, вы можете найти меня в LinkedIn. Спасибо за чтение!

Обновлять!!!

Как было указано в комментариях, вам на самом деле не нужно передавать newObject вспомогательной функции flatten в качестве аргумента, потому что значение newObject закрывается во время вызова функции. Приведенный выше код по-прежнему будет работать, но вы можете просто удалить ссылки на newObject в любом вызове функции flattenObject, а также в определении ее функции, чтобы удалить ненужную ссылку. Спасибо Крису Россу за исправление!