Loading...
X

Рекурсивный перебор вложенных объектов на JavaScript

Допустим, есть объект, который в качестве своих полей может содержать другие объекты, которые, в свою очередь, могут содержать другие объекты и т.д., либо содержать конечное значение любого типа, т.е., у нас есть фракталоподобный объект. Задача состоит в том, чтобы перебрать все конечные значения каждого поля исходного объекта и сделать что-то полезное.

К примеру, у нас есть объект:
    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

Оставьте свой комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *