Рекурсивно перебирать объект для создания списка свойств

Ситуация: у меня есть большой объект, содержащий несколько суб и суб-объектов, со свойствами, содержащими несколько типов данных. Для наших целей этот объект выглядит примерно так:

var object = {
    aProperty: {
        aSetting1: 1,
        aSetting2: 2,
        aSetting3: 3,
        aSetting4: 4,
        aSetting5: 5
    },
    bProperty: {
        bSetting1: {
            bPropertySubSetting : true
        },
        bSetting2: "bString"
    },
    cProperty: {
        cSetting: "cString"
    }
}

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

aProperty.aSetting1
aProperty.aSetting2
aProperty.aSetting3
aProperty.aSetting4
aProperty.aSetting5
bProperty.bSetting1.bPropertySubSetting
bProperty.bSetting2
cProperty.cSetting

У меня есть эта функция, которая выполняет цикл через объект и выплевывает ключи, но не иерархически:

function iterate(obj) {
    for (var property in obj) {
        if (obj.hasOwnProperty(property)) {
            if (typeof obj[property] == "object") {
                iterate(obj[property]);
            }
            else {
                console.log(property + "   " + obj[property]);
            }
        }
    }
}

Может кто-нибудь дать мне знать, как это сделать? Вот вам jsfiddle: http://jsfiddle.net/tbynA/

Ответ 1

Я сделал для вас FIDDLE. Я сохраняю строку stack, а затем выводю ее, если свойство имеет примитивный тип:

function iterate(obj, stack) {
        for (var property in obj) {
            if (obj.hasOwnProperty(property)) {
                if (typeof obj[property] == "object") {
                    iterate(obj[property], stack + '.' + property);
                } else {
                    console.log(property + "   " + obj[property]);
                    $('#output').append($("<div/>").text(stack + '.' + property))
                }
            }
        }
    }

iterate(object, '')

ОБНОВЛЕНИЕ (24/07/2017)

В последнее время я получил много комментариев для этого вопроса, поэтому решил уточнить решение с помощью волшебства ES2015 + и более функционального стиля.

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

const isObject = val =>
  typeof val === 'object' && !Array.isArray(val);

const paths = (obj = {}) =>
  Object.entries(obj)
    .reduce(
      (product, [key, value]) =>
        isObject(value) ?
        product.concat([
          [key, paths(value)] // adds [root, [children]] list
        ]) :
        product.concat([key]), // adds [child] list
      []
    )

const addDelimiter = (a, b) =>
  a ? `${a}.${b}` : b;

const pathToString = ([root, children]) =>
  children.map(
    child =>
      Array.isArray(child) ?
      addDelimiter(root, pathToString(child)) :
      addDelimiter(root, child)
  )
  .join('\n');

const input = {
  aProperty: {
    aSetting1: 1,
    aSetting2: 2,
    aSetting3: 3,
    aSetting4: 4,
    aSetting5: 5
  },
  bProperty: {
    bSetting1: {
      bPropertySubSetting: true
    },
    bSetting2: "bString"
  },
  cProperty: {
    cSetting: "cString"
  }
};

// ^ implies a "root" level will be ["", paths(input)]
// ideally paths() should return that structure, but I could not figure out how :)
// shows desired output format
console.log(pathToString(["", paths(input)]));

// showcase the resulting data structure
// any object can be recursively represented as a list [objectPropertyName, [...nestedPropertyNames]]
// console.log(paths(input));

Ответ 2

У вас возникнут проблемы с этим, если объект имеет цикл в своем объектном графе, например:

var object = {
    aProperty: {
        aSetting1: 1
    },
};
object.ref = object;

В этом случае вы можете захотеть сохранить ссылки на объекты, которые вы уже прошли, и исключить их из итерации.

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

var object = {
  a: { b: { c: { ... }} }
};

Вы получите слишком много ошибок при рекурсивных вызовах. Оба можно избежать:

function iterate(obj) {
    var walked = [];
    var stack = [{obj: obj, stack: ''}];
    while(stack.length > 0)
    {
        var item = stack.pop();
        var obj = item.obj;
        for (var property in obj) {
            if (obj.hasOwnProperty(property)) {
                if (typeof obj[property] == "object") {
                  var alreadyFound = false;
                  for(var i = 0; i < walked.length; i++)
                  {
                    if (walked[i] === obj[property])
                    {
                      alreadyFound = true;
                      break;
                    }
                  }
                  if (!alreadyFound)
                  {
                    walked.push(obj[property]);
                    stack.push({obj: obj[property], stack: item.stack + '.' + property});
                  }
                }
                else
                {
                    console.log(item.stack + '.' + property + "=" + obj[property]);
                }
            }
        }
    }
}

iterate(object); 

Ответ 3

https://github.com/hughsk/flat

var flatten = require('flat')
flatten({
key1: {
    keyA: 'valueI'
},
key2: {
    keyB: 'valueII'
},
key3: { a: { b: { c: 2 } } }
})

// {
//   'key1.keyA': 'valueI',
//   'key2.keyB': 'valueII',
//   'key3.a.b.c': 2
// }

Просто цикл, чтобы получить индексы после.

Ответ 4

Вам не нужна рекурсия!

Следующая функция функции молниеносной функции, которая выводит записи в порядке наименее глубокого до самого глубокого значения со значением ключа в качестве массива [key, value].

function deepEntries( obj ){
    'use-strict';
    var allkeys, curKey = '[', len = 0, i = -1, entryK;

    function formatKeys( entries ){
       entryK = entries.length;
       len += entries.length;
       while (entryK--)
         entries[entryK][0] = curKey+JSON.stringify(entries[entryK][0])+']';
       return entries;
    }
    allkeys = formatKeys( Object.entries(obj) );

    while (++i !== len)
        if (typeof allkeys[i][1] === 'object' && allkeys[i][1] !== null){
            curKey = allkeys[i][0] + '[';
            Array.prototype.push.apply(
                allkeys,
                formatKeys( Object.entries(allkeys[i][1]) )
            );
        }
    return allkeys;
}

Затем, чтобы вывести результаты, которые вы ищете, просто используйте это.

function stringifyEntries(allkeys){
    return allkeys.reduce(function(acc, x){
        return acc+((acc&&'\n')+x[0])
    }, '');
};

Если вы заинтересованы в технических битах, то вот как это работает. Он работает, получая Object.entries объекта obj, который вы передали, и помещает их в массив allkeys. Затем, перейдя от начала allkeys до конца, если он обнаружит, что одно из значений allkeys entries - это объект, то он получает ключ entrie как curKey и префикс каждого из его собственных записей с помощью curKey до того, как он вытолкнет полученный результирующий массив в конец allkeys. Затем он добавляет количество записей, добавленных к allkeys, к заданной длине, так что оно также будет переходить к этим вновь добавленным клавишам.

Например, обратите внимание на следующее:

<script>
var object = {
    aProperty: {
        aSetting1: 1,
        aSetting2: 2,
        aSetting3: 3,
        aSetting4: 4,
        aSetting5: 5
    },
    bProperty: {
        bSetting1: {
            bPropertySubSetting : true
        },
        bSetting2: "bString"
    },
    cProperty: {
        cSetting: "cString"
    }
}
document.write(
    '<pre>' + stringifyEntries( deepEntries(object) ) + '</pre>'
);
function deepEntries( obj ){//debugger;
    'use-strict';
    var allkeys, curKey = '[', len = 0, i = -1, entryK;

    function formatKeys( entries ){
       entryK = entries.length;
       len += entries.length;
       while (entryK--)
         entries[entryK][0] = curKey+JSON.stringify(entries[entryK][0])+']';
       return entries;
    }
    allkeys = formatKeys( Object.entries(obj) );

    while (++i !== len)
        if (typeof allkeys[i][1] === 'object' && allkeys[i][1] !== null){
            curKey = allkeys[i][0] + '[';
            Array.prototype.push.apply(
                allkeys,
                formatKeys( Object.entries(allkeys[i][1]) )
            );
        }
    return allkeys;
}
function stringifyEntries(allkeys){
    return allkeys.reduce(function(acc, x){
        return acc+((acc&&'\n')+x[0])
    }, '');
};
</script>

Ответ 5

ОБНОВЛЕНИЕ: ТОЛЬКО ИСПОЛЬЗОВАТЬ JSON.stringify для печати объектов на экране!

Все, что вам нужно, это эта строка:

document.body.innerHTML = '<pre>' + JSON.stringify(ObjectWithSubObjects, null, "\t") + '</pre>';

Это моя старая версия рекурсивно печатающих объектов на экране:

 var previousStack = '';
    var output = '';
    function objToString(obj, stack) {
        for (var property in obj) {
            var tab = '&nbsp;&nbsp;&nbsp;&nbsp;';
            if (obj.hasOwnProperty(property)) {
                if (typeof obj[property] === 'object' && typeof stack === 'undefined') {
                    config = objToString(obj[property], property);
                } else {
                    if (typeof stack !== 'undefined' && stack !== null && stack === previousStack) {
                        output = output.substring(0, output.length - 1);  // remove last }
                        output += tab + '<span>' + property + ': ' + obj[property] + '</span><br />'; // insert property
                        output += '}';   // add last } again
                    } else {
                        if (typeof stack !== 'undefined') {
                            output += stack + ': {  <br />' + tab;
                        }
                        output += '<span>' + property + ': ' + obj[property] + '</span><br />';
                        if (typeof stack !== 'undefined') {
                            output += '}';
                        }
                    }
                    previousStack = stack;
                }
            }
        }
        return output;
    }

Использование:

document.body.innerHTML = objToString(ObjectWithSubObjects);

Пример вывода:

cache: false
position: fixed
effect: { 
    fade: false
    fall: true
}

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

Ответ 6

Предположим, что у вас есть объект JSON, например:

var example = {
    "prop1": "value1",
    "prop2": [ "value2_0", "value2_1"],
    "prop3": {
         "prop3_1": "value3_1"
    }
}

Неправильный путь для итерации через свои свойства:

function recursivelyIterateProperties(jsonObject) {
    for (var prop in Object.keys(jsonObject)) {
        console.log(prop);
        recursivelyIterateProperties(jsonObject[prop]);
    }
}

Вы можете быть удивлены, увидев ведение журнала консоли 0, 1 и т.д. при повторении с помощью свойств prop1 и prop2 и prop3_1. Эти объекты являются последовательностями, а индексы последовательности являются свойствами этого объекта в Javascript.

Лучший способ рекурсивного итерации через свойства объекта JSON - это сначала проверить, является ли этот объект последовательностью или нет:

function recursivelyIterateProperties(jsonObject) {
    for (var prop in Object.keys(jsonObject)) {
        console.log(prop);
        if (!(typeof(jsonObject[prop]) === 'string')
            && !(jsonObject[prop] instanceof Array)) {
                recursivelyIterateProperties(jsonObject[prop]);

            }
     }
}

Если вы хотите найти свойства внутри объектов в массивах, выполните следующие действия:

function recursivelyIterateProperties(jsonObject) {

    if (jsonObject instanceof Array) {
        for (var i = 0; i < jsonObject.length; ++i) {
            recursivelyIterateProperties(jsonObject[i])
        }
    }
    else if (typeof(jsonObject) === 'object') {
        for (var prop in Object.keys(jsonObject)) {
            console.log(prop);
            if (!(typeof(jsonObject[prop]) === 'string')) {
                recursivelyIterateProperties(jsonObject[prop]);
            }
        }
    }
}

Ответ 7

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

[ "aProperty.aSetting1", "aProperty.aSetting2", "aProperty.aSetting3", "aProperty.aSetting4", "aProperty.aSetting5", "bProperty.bSetting1.bPropertySubSetting", "bProperty.bSetting2", "cProperty.cSetting" ]

 /**
 * Recursively searches for properties in a given object. 
 * Ignores possible prototype endless enclosures. 
 * Can list either all properties or filtered by key name.
 *
 * @param {Object} object Object with properties.
 * @param {String} key Property key name to search for. Empty string to 
 *                     get all properties list .
 * @returns {String} Paths to properties from object root.
 */
function getPropertiesByKey(object, key) {

  var paths = [
  ];

  iterate(
    object,
    "");

  return paths;

  /**
   * Single object iteration. Accumulates to an outer 'paths' array.
   */
  function iterate(object, path) {
    var chainedPath;

    for (var property in object) {
      if (object.hasOwnProperty(property)) {

        chainedPath =
          path.length > 0 ?
          path + "." + property :
          path + property;

        if (typeof object[property] == "object") {

          iterate(
            object[property],
            chainedPath,
            chainedPath);
        } else if (
          property === key ||
          key.length === 0) {

          paths.push(
            chainedPath);
        }
      }
    }

    return paths;
  }
}

Ответ 8

Эта версия упакована в функцию, которая принимает пользовательский разделитель, фильтрует и возвращает плоский словарь:

function flatten(source, delimiter, filter) {
  var result = {}
  ;(function flat(obj, stack) {
    Object.keys(obj).forEach(function(k) {
      var s = stack.concat([k])
      var v = obj[k]
      if (filter && filter(k, v)) return
      if (typeof v === 'object') flat(v, s)
      else result[s.join(delimiter)] = v
    })
  })(source, [])
  return result
}
var obj = {
  a: 1,
  b: {
    c: 2
  }
}
flatten(obj)
// <- Object {a: 1, b.c: 2}
flatten(obj, '/')
// <- Object {a: 1, b/c: 2}
flatten(obj, '/', function(k, v) { return k.startsWith('a') })
// <- Object {b/c: 2}

Ответ 9

Решение от Artyom Neustroev не работает над сложными объектами, так что это рабочее решение, основанное на его идее:

function propertiesToArray(obj) {
    const isObject = val =>
        typeof val === 'object' && !Array.isArray(val);

    const addDelimiter = (a, b) =>
        a ? '${a}.${b}' : b;

    const paths = (obj = {}, head = '') => {
        return Object.entries(obj)
            .reduce((product, [key, value]) => 
                {
                    let fullPath = addDelimiter(head, key)
                    return isObject(value) ?
                        product.concat(paths(value, fullPath))
                    : product.concat(fullPath)
                }, []);
    }

    return paths(obj);
}