Рекурсивный перебор вложенных объектов на JavaScript
Опубликовано Romanzhivo - 8 июня 2018, 20:15
Допустим, есть объект, который в качестве своих полей может содержать другие объекты, которые, в свою очередь, могут содержать другие объекты и т.д., либо содержать конечное значение любого типа, т.е., у нас есть фракталоподобный объект. Задача состоит в том, чтобы перебрать все конечные значения каждого поля исходного объекта и сделать что-то полезное.
К примеру, у нас есть объект:
var fractal = {
a1: {
b1: {
c: 1
},
b2: {
c: 222
},
b3: {
c: {
d: 33,
e: 2.5,
f: {
g: 9999,
h: {
i: {
j: 1001,
k: 'строка',
l: [1,2,3],
m: function() {}
}
}
}
}
}
}
}
Как получить все конечные значения оптимальным способом?
Чтобы перебрать все поля и узнать значения, мы могли бы посчитать уровни вложенности полей и написать рекурсивную функцию, которая перебирала бы строго определённое количество свойств в объекте, например, что-то вроде:
function getProp(obj) {
for(var prop in obj) {
if(typeof(obj[prop]) === 'object') {
console.log(obj[prop])
for(var prop2 in obj[prop]) {
if(typeof(obj[prop][prop2]) === 'object') {
console.log(obj[prop][prop2])
//... другие вложенные циклы for...in
}
}
}
}
}
Но это плохое решение, т.к. мы вынуждены дублировать код, он получается громоздким, и при этом мы не можем быть уверены, что переданный при вызове функции объект сохранит то же число вложенных полей, на которое мы и написали функцию.
Поэтому более правильно будет использовать рекурсивную функцию, которая не зависит от конкретного количества уровней вложенности свойств. Например, такую:
function getFiniteValue(obj) {
getProp(obj);
function getProp(o) {
for(var prop in o) {
if(typeof(o[prop]) === 'object') {
getProp(o[prop]);
} else {
console.log('Finite value: ',o[prop])
}
}
}
}
Таким образом, мы можем передавать объект с любым количеством уровней вложенности свойств и получать конечные значения каждого свойства, которое не является объектом:
var fractal = {
a1: {
b1: {
c: 1
},
b2: {
c: 222
},
b3: {
c: {
d: 33,
e: 2.5,
f: {
g: 9999,
h: {
i: {
j: 1001,
k: 'строка',
l: [1,2,3],
m: function() {}
}
}
}
}
}
}
}
getFiniteValue(fractal);
function getFiniteValue(obj) {
getProp(obj);
function getProp(o) {
for(var prop in o) {
if(typeof(o[prop]) === 'object') {
getProp(o[prop]);
} else {
console.log('Finite value: '+o[prop])
}
}
}
}
Пример на JSFiddle
Однако, как в комментариях на stackoverflow заметил пользователь Yaant, существует неочевидная проблема: если одно из свойств объекта будет циклической ссылкой, т.е. будет ссылаться на одно из своих родительских свойств, то рекурсивный вызов функции окажется бесконечным, что в итоге приведёт к переполнению стека вызовов функции.
Решением проблемы может быть сохранение информации о факте перебора объекта в цикле и вывод информации о том, что объект имеет циклическую ссылку. Для этого можно добавлять обрабатываемому в цикле объекту временное свойство-флаг, а затем удалять его. Лучше выбрать максимально уникальное имя, чтобы оно случайно не совпало с уже имеющимся у объекта свойством. Например, запишем временное свойство temp__isAlreadyHandled__ каждому объекту, который был обработан в цикле, а затем удалим это свойство:
function getFiniteValue(obj) {
var handledFlag = 'temp__isAlreadyHandled__';
getProp(obj);
function getProp(o, stack) {
var propertyPath;
for(var prop in o) {
if(typeof(o[prop]) === 'object') {
if(!o[prop][handledFlag]) {
Object.defineProperty(o[prop],handledFlag, {
value: true,
writable:false,
configurable: true
});
if(!stack)
propertyPath = 'rootObject.' + prop
else
propertyPath = stack + '.' + prop;
getProp(o[prop], propertyPath);
} else {
propertyPath = stack + '.' + prop;
console.error('Циклическая ссылка. Свойство: ' + propertyPath);
}
delete o[prop][handledFlag]
} else {
console.log('Finite value: ',o[prop])
}
}
}
}
Пример на JSFiddle
Получить все целочисленные значения
Также можно добавлять и другие полезные функции. Например, мы хотим получить все целочисленные значения из всех дочерних полей-объектов. Внутри функции getFiniteValue мы можем определить функцию isInteger, которая будет проверять входящий аргумент на соответствие целочисленному значению:
function getFiniteValue(obj) {
var handledFlag = 'temp__isAlreadyHandled__';
getProp(obj);
function getProp(o, stack) {
var propertyPath;
for(var prop in o) {
if(typeof(o[prop]) === 'object') {
if(!o[prop][handledFlag]) {
Object.defineProperty(o[prop],handledFlag, {
value: true,
writable:false,
configurable: true
});
if(!stack)
propertyPath = 'rootObject.' + prop
else
propertyPath = stack + '.' + prop;
getProp(o[prop], propertyPath);
} else {
propertyPath = stack + '.' + prop;
console.error('Циклическая ссылка. Свойство: ' + propertyPath);
}
delete o[prop][handledFlag]
} else if(typeof(o[prop]) === 'number' && isInteger(o[prop])) {
console.log('Integer: ',o[prop], isInteger(o[prop]))
}
}
}
function isInteger(num) {
return (num ^ 0) === num;
}
}
Пример на JSFiddle





