Рекурсивный перебор вложенных объектов на 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