Рекурсивно перебирать объект для создания списка свойств
Ситуация: у меня есть большой объект, содержащий несколько суб и суб-объектов, со свойствами, содержащими несколько типов данных. Для наших целей этот объект выглядит примерно так:
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 = ' ';
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);
}