Iterate through Nested JavaScript Objects
JavascriptIterationJavascript Problem Overview
I'm trying to iterate through a nested object to retrieve a specific object identified by a string. In the sample object below, the identifier string is the "label" property. I can't wrap my head around how to iterate down through the tree to return the appropriate object. Any help or suggestions would be greatly appreciated.
var cars = {
label: 'Autos',
subs: [
{
label: 'SUVs',
subs: []
},
{
label: 'Trucks',
subs: [
{
label: '2 Wheel Drive',
subs: []
},
{
label: '4 Wheel Drive',
subs: [
{
label: 'Ford',
subs: []
},
{
label: 'Chevrolet',
subs: []
}
]
}
]
},
{
label: 'Sedan',
subs: []
}
]
}
Javascript Solutions
Solution 1 - Javascript
In case you want to deeply iterate into a complex (nested) object for each key & value, you can do so using Object.keys(), recursively:
const iterate = (obj) => {
Object.keys(obj).forEach(key => {
console.log(`key: ${key}, value: ${obj[key]}`)
if (typeof obj[key] === 'object' && obj[key] !== null) {
iterate(obj[key])
}
})
}
Solution 2 - Javascript
You can create a recursive function like this to do a depth-first traversal of the cars
object.
var findObjectByLabel = function(obj, label) {
if(obj.label === label) { return obj; }
for(var i in obj) {
if(obj.hasOwnProperty(i)){
var foundLabel = findObjectByLabel(obj[i], label);
if(foundLabel) { return foundLabel; }
}
}
return null;
};
which can be called like so
findObjectByLabel(car, "Chevrolet");
Solution 3 - Javascript
헗헲헮헱-혀헶헺헽헹헲 현헶혁헵 ퟯ 혃헮헿헶헮헯헹헲혀, ퟵ 헹헶헻헲혀, 헮헻헱 헻헼 헿헲헰혂헿혀헶헼헻
function forEachNested(O, f, cur){
O = [ O ]; // ensure that f is called with the top-level object
while (O.length) // keep on processing the top item on the stack
if(
!f( cur = O.pop() ) && // do not spider down if `f` returns true
cur instanceof Object && // ensure cur is an object, but not null
[Object, Array].includes(cur.constructor) //limit search to [] and {}
) O.push.apply(O, Object.values(cur)); //search all values deeper inside
}
To use the above function, pass the array as the first argument and the callback function as the second argument. The callback function will receive 1 argument when called: the current item being iterated.
(function(){"use strict";
var cars = {"label":"Autos","subs":[{"label":"SUVs","subs":[]},{"label":"Trucks","subs":[{"label":"2 Wheel Drive","subs":[]},{"label":"4 Wheel Drive","subs":[{"label":"Ford","subs":[]},{"label":"Chevrolet","subs":[]}]}]},{"label":"Sedan","subs":[]}]};
var lookForCar = prompt("enter the name of the car you are looking for (e.g. 'Ford')") || 'Ford';
lookForCar = lookForCar.replace(/[^ \w]/g, ""); // incaseif the user put quotes or something around their input
lookForCar = lookForCar.toLowerCase();
var foundObject = null;
forEachNested(cars, function(currentValue){
if(currentValue.constructor === Object &&
currentValue.label.toLowerCase() === lookForCar) {
foundObject = currentValue;
}
});
if (foundObject !== null) {
console.log("Found the object: " + JSON.stringify(foundObject, null, "\t"));
} else {
console.log('Nothing found with a label of "' + lookForCar + '" :(');
}
function forEachNested(O, f, cur){
O = [ O ]; // ensure that f is called with the top-level object
while (O.length) // keep on processing the top item on the stack
if(
!f( cur = O.pop() ) && // do not spider down if `f` returns true
cur instanceof Object && // ensure cur is an object, but not null
[Object, Array].includes(cur.constructor) //limit search to [] and {}
) O.push.apply(O, Object.values(cur)); //search all values deeper inside
}
})();
A "cheat" alternative might be to use JSON.stringify
to iterate. HOWEVER, JSON.stringify
will call the toString
method of each object it passes over, which may produce undesirable results if you have your own special uses for the toString
.
function forEachNested(O, f, v){
typeof O === "function" ? O(v) : JSON.stringify(O,forEachNested.bind(0,f));
return v; // so that JSON.stringify keeps on recursing
}
(function(){"use strict";
var cars = {"label":"Autos","subs":[{"label":"SUVs","subs":[]},{"label":"Trucks","subs":[{"label":"2 Wheel Drive","subs":[]},{"label":"4 Wheel Drive","subs":[{"label":"Ford","subs":[]},{"label":"Chevrolet","subs":[]}]}]},{"label":"Sedan","subs":[]}]};
var lookForCar = prompt("enter the name of the car you are looking for (e.g. 'Ford')") || 'Ford';
lookForCar = lookForCar.replace(/[^ \w]/g, ""); // incaseif the user put quotes or something around their input
lookForCar = lookForCar.toLowerCase();
var foundObject = null;
forEachNested(cars, function(currentValue){
if(currentValue.constructor === Object &&
currentValue.label.toLowerCase() === lookForCar) {
foundObject = currentValue;
}
});
if (foundObject !== null)
console.log("Found the object: " + JSON.stringify(foundObject, null, "\t"));
else
console.log('Nothing found with a label of "' + lookForCar + '" :(');
function forEachNested(O, f, v){
typeof O === "function" ? O(v) : JSON.stringify(O,forEachNested.bind(0,f));
return v; // so that JSON.stringify keeps on recursing
}
})();
However, while the above method might be useful for demonstration purposes, Object.values
is not supported by Internet Explorer and there are many terribly illperformant places in the code:
- the code changes the value of input parameters (arguments) [lines 2 & 5],
- the code calls
Array.prototype.push
andArray.prototype.pop
on every single item [lines 5 & 8], - the code only does a pointer-comparison for the constructor which does not work on out-of-window objects [line 7],
- the code duplicates the array returned from
Object.values
[line 8], - the code does not localize
window.Object
orwindow.Object.values
[line 9], - and the code needlessly calls Object.values on arrays [line 8].
Below is a much much faster version that should be far faster than any other solution. The solution below fixes all of the performance problems listed above. However, it iterates in a much different way: it iterates all the arrays first, then iterates all the objects. It continues to iterate its present type until complete exhaustion including iteration subvalues inside the current list of the current flavor being iterated. Then, the function iterates all of the other type. By iterating until exhaustion before switching over, the iteration loop gets hotter than otherwise and iterates even faster. This method also comes with an added advantage: the callback which is called on each value gets passed a second parameter. This second parameter is the array returned from Object.values
called on the parent hash Object, or the parent Array itself.
var getValues = Object.values; // localize
var type_toString = Object.prototype.toString;
function forEachNested(objectIn, functionOnEach){
"use strict";
functionOnEach( objectIn );
// for iterating arbitrary objects:
var allLists = [ ];
if (type_toString.call( objectIn ) === '[object Object]')
allLists.push( getValues(objectIn) );
var allListsSize = allLists.length|0; // the length of allLists
var indexLists = 0;
// for iterating arrays:
var allArray = [ ];
if (type_toString.call( objectIn ) === '[object Array]')
allArray.push( objectIn );
var allArraySize = allArray.length|0; // the length of allArray
var indexArray = 0;
do {
// keep cycling back and forth between objects and arrays
for ( ; indexArray < allArraySize; indexArray=indexArray+1|0) {
var currentArray = allArray[indexArray];
var currentLength = currentArray.length;
for (var curI=0; curI < currentLength; curI=curI+1|0) {
var arrayItemInner = currentArray[curI];
if (arrayItemInner === undefined &&
!currentArray.hasOwnProperty(arrayItemInner)) {
continue; // the value at this position doesn't exist!
}
functionOnEach(arrayItemInner, currentArray);
if (typeof arrayItemInner === 'object') {
var typeTag = type_toString.call( arrayItemInner );
if (typeTag === '[object Object]') {
// Array.prototype.push returns the new length
allListsSize=allLists.push( getValues(arrayItemInner) );
} else if (typeTag === '[object Array]') {
allArraySize=allArray.push( arrayItemInner );
}
}
}
allArray[indexArray] = null; // free up memory to reduce overhead
}
for ( ; indexLists < allListsSize; indexLists=indexLists+1|0) {
var currentList = allLists[indexLists];
var currentLength = currentList.length;
for (var curI=0; curI < currentLength; curI=curI+1|0) {
var listItemInner = currentList[curI];
functionOnEach(listItemInner, currentList);
if (typeof listItemInner === 'object') {
var typeTag = type_toString.call( listItemInner );
if (typeTag === '[object Object]') {
// Array.prototype.push returns the new length
allListsSize=allLists.push( getValues(listItemInner) );
} else if (typeTag === '[object Array]') {
allArraySize=allArray.push( listItemInner );
}
}
}
allLists[indexLists] = null; // free up memory to reduce overhead
}
} while (indexLists < allListsSize || indexArray < allArraySize);
}
(function(){"use strict";
var cars = {"label":"Autos","subs":[{"label":"SUVs","subs":[]},{"label":"Trucks","subs":[{"label":"2 Wheel Drive","subs":[]},{"label":"4 Wheel Drive","subs":[{"label":"Ford","subs":[]},{"label":"Chevrolet","subs":[]}]}]},{"label":"Sedan","subs":[]}]};
var lookForCar = prompt("enter the name of the car you are looking for (e.g. 'Ford')") || 'Ford';
lookForCar = lookForCar.replace(/[^ \w]/g, ""); // incaseif the user put quotes or something around their input
lookForCar = lookForCar.toLowerCase();
var getValues = Object.values; // localize
var type_toString = Object.prototype.toString;
function forEachNested(objectIn, functionOnEach){
functionOnEach( objectIn );
// for iterating arbitrary objects:
var allLists = [ ];
if (type_toString.call( objectIn ) === '[object Object]')
allLists.push( getValues(objectIn) );
var allListsSize = allLists.length|0; // the length of allLists
var indexLists = 0;
// for iterating arrays:
var allArray = [ ];
if (type_toString.call( objectIn ) === '[object Array]')
allArray.push( objectIn );
var allArraySize = allArray.length|0; // the length of allArray
var indexArray = 0;
do {
// keep cycling back and forth between objects and arrays
for ( ; indexArray < allArraySize; indexArray=indexArray+1|0) {
var currentArray = allArray[indexArray];
var currentLength = currentArray.length;
for (var curI=0; curI < currentLength; curI=curI+1|0) {
var arrayItemInner = currentArray[curI];
if (arrayItemInner === undefined &&
!currentArray.hasOwnProperty(arrayItemInner)) {
continue; // the value at this position doesn't exist!
}
functionOnEach(arrayItemInner, currentArray);
if (typeof arrayItemInner === 'object') {
var typeTag = type_toString.call( arrayItemInner );
if (typeTag === '[object Object]') {
// Array.prototype.push returns the new length
allListsSize=allLists.push( getValues(arrayItemInner) );
} else if (typeTag === '[object Array]') {
allArraySize=allArray.push( arrayItemInner );
}
}
}
allArray[indexArray] = null; // free up memory to reduce overhead
}
for ( ; indexLists < allListsSize; indexLists=indexLists+1|0) {
var currentList = allLists[indexLists];
var currentLength = currentList.length;
for (var curI=0; curI < currentLength; curI=curI+1|0) {
var listItemInner = currentList[curI];
functionOnEach(listItemInner, currentList);
if (typeof listItemInner === 'object') {
var typeTag = type_toString.call( listItemInner );
if (typeTag === '[object Object]') {
// Array.prototype.push returns the new length
allListsSize=allLists.push( getValues(listItemInner) );
} else if (typeTag === '[object Array]') {
allArraySize=allArray.push( listItemInner );
}
}
}
allLists[indexLists] = null; // free up memory to reduce overhead
}
} while (indexLists < allListsSize || indexArray < allArraySize);
}
var foundObject = null;
forEachNested(cars, function(currentValue){
if(currentValue.constructor === Object &&
currentValue.label.toLowerCase() === lookForCar) {
foundObject = currentValue;
}
});
if (foundObject !== null) {
console.log("Found the object: " + JSON.stringify(foundObject, null, "\t"));
} else {
console.log('Nothing found with a label of "' + lookForCar + '" :(');
}
})();
If you have a problem with circular references (e.g. having object A's values being object A itself in such as that object A contains itself), or you just need the keys then the following slower solution is available.
function forEachNested(O, f){
O = Object.entries(O);
var cur;
function applyToEach(x){return cur[1][x[0]] === x[1]}
while (O.length){
cur = O.pop();
f(cur[0], cur[1]);
if (typeof cur[1] === 'object' && cur[1].constructor === Object &&
!O.some(applyToEach))
O.push.apply(O, Object.entries(cur[1]));
}
}
Because these methods do not use any recursion of any sort, these functions are well suited for areas where you might have thousands of levels of depth. The stack limit varies greatly from browser to browser, so recursion to an unknown depth is not very wise in Javascript.
Solution 4 - Javascript
The following code assumes no circular references, and assumes subs
is always an array (and not null in leaf nodes):
function find(haystack, needle) {
if (haystack.label === needle) return haystack;
for (var i = 0; i < haystack.subs.length; i ++) {
var result = find(haystack.subs[i], needle);
if (result) return result;
}
return null;
}
Solution 5 - Javascript
You can get through every object in the list and get which value you want. Just pass an object as first parameter in the function call and object property which you want as second parameter. Change object with your object.
const treeData = [{
"jssType": "fieldset",
"jssSelectLabel": "Fieldset (with legend)",
"jssSelectGroup": "jssItem",
"jsName": "fieldset-715",
"jssLabel": "Legend",
"jssIcon": "typcn typcn-folder",
"expanded": true,
"children": [{
"jssType": "list-ol",
"jssSelectLabel": "List - ol",
"jssSelectGroup": "jssItem",
"jsName": "list-ol-147",
"jssLabel": "",
"jssIcon": "dashicons dashicons-editor-ol",
"noChildren": false,
"expanded": true,
"children": [{
"jssType": "list-li",
"jssSelectLabel": "List Item - li",
"jssSelectGroup": "jssItem",
"jsName": "list-li-752",
"jssLabel": "",
"jssIcon": "dashicons dashicons-editor-ul",
"noChildren": false,
"expanded": true,
"children": [{
"jssType": "text",
"jssSelectLabel": "Text (short text)",
"jssSelectGroup": "jsTag",
"jsName": "text-422",
"jssLabel": "Your Name (required)",
"jsRequired": true,
"jsTagOptions": [{
"jsOption": "",
"optionLabel": "Default value",
"optionType": "input"
},
{
"jsOption": "placeholder",
"isChecked": false,
"optionLabel": "Use this text as the placeholder of the field",
"optionType": "checkbox"
},
{
"jsOption": "akismet_author_email",
"isChecked": false,
"optionLabel": "Akismet - this field requires author's email address",
"optionType": "checkbox"
}
],
"jsValues": "",
"jsPlaceholder": false,
"jsAkismetAuthor": false,
"jsIdAttribute": "",
"jsClassAttribute": "",
"jssIcon": "typcn typcn-sort-alphabetically",
"noChildren": true
}]
},
{
"jssType": "list-li",
"jssSelectLabel": "List Item - li",
"jssSelectGroup": "jssItem",
"jsName": "list-li-538",
"jssLabel": "",
"jssIcon": "dashicons dashicons-editor-ul",
"noChildren": false,
"expanded": true,
"children": [{
"jssType": "email",
"jssSelectLabel": "Email",
"jssSelectGroup": "jsTag",
"jsName": "email-842",
"jssLabel": "Email Address (required)",
"jsRequired": true,
"jsTagOptions": [{
"jsOption": "",
"optionLabel": "Default value",
"optionType": "input"
},
{
"jsOption": "placeholder",
"isChecked": false,
"optionLabel": "Use this text as the placeholder of the field",
"optionType": "checkbox"
},
{
"jsOption": "akismet_author_email",
"isChecked": false,
"optionLabel": "Akismet - this field requires author's email address",
"optionType": "checkbox"
}
],
"jsValues": "",
"jsPlaceholder": false,
"jsAkismetAuthorEmail": false,
"jsIdAttribute": "",
"jsClassAttribute": "",
"jssIcon": "typcn typcn-mail",
"noChildren": true
}]
},
{
"jssType": "list-li",
"jssSelectLabel": "List Item - li",
"jssSelectGroup": "jssItem",
"jsName": "list-li-855",
"jssLabel": "",
"jssIcon": "dashicons dashicons-editor-ul",
"noChildren": false,
"expanded": true,
"children": [{
"jssType": "textarea",
"jssSelectLabel": "Textarea (long text)",
"jssSelectGroup": "jsTag",
"jsName": "textarea-217",
"jssLabel": "Your Message",
"jsRequired": false,
"jsTagOptions": [{
"jsOption": "",
"optionLabel": "Default value",
"optionType": "input"
},
{
"jsOption": "placeholder",
"isChecked": false,
"optionLabel": "Use this text as the placeholder of the field",
"optionType": "checkbox"
}
],
"jsValues": "",
"jsPlaceholder": false,
"jsIdAttribute": "",
"jsClassAttribute": "",
"jssIcon": "typcn typcn-document-text",
"noChildren": true
}]
}
]
},
{
"jssType": "paragraph",
"jssSelectLabel": "Paragraph - p",
"jssSelectGroup": "jssItem",
"jsName": "paragraph-993",
"jssContent": "* Required",
"jssIcon": "dashicons dashicons-editor-paragraph",
"noChildren": true
}
]
},
{
"jssType": "submit",
"jssSelectLabel": "Submit",
"jssSelectGroup": "jsTag",
"jsName": "submit-704",
"jssLabel": "Send",
"jsValues": "",
"jsRequired": false,
"jsIdAttribute": "",
"jsClassAttribute": "",
"jssIcon": "typcn typcn-mail",
"noChildren": true
},
];
function findObjectByLabel(obj, label) {
for(var elements in obj){
if (elements === label){
console.log(obj[elements]);
}
if(typeof obj[elements] === 'object'){
findObjectByLabel(obj[elements], 'jssType');
}
}
};
findObjectByLabel(treeData, 'jssType');
Solution 6 - Javascript
Here is a concise breadth-first iterative solution, which I prefer to recursion:
const findCar = function(car) {
const carSearch = [cars];
while(carSearch.length) {
let item = carSearch.shift();
if (item.label === car) return true;
carSearch.push(...item.subs);
}
return false;
}
Solution 7 - Javascript
Here is a solution using object-scan
// const objectScan = require('object-scan');
const cars = { label: 'Autos', subs: [ { label: 'SUVs', subs: [] }, { label: 'Trucks', subs: [ { label: '2 Wheel Drive', subs: [] }, { label: '4 Wheel Drive', subs: [ { label: 'Ford', subs: [] }, { label: 'Chevrolet', subs: [] } ] } ] }, { label: 'Sedan', subs: [] } ] };
const find = (haystack, label) => objectScan(['**.label'], {
filterFn: ({ value }) => value === label,
rtn: 'parent',
abort: true
})(haystack);
console.log(find(cars, 'Sedan'));
// => { label: 'Sedan', subs: [] }
console.log(find(cars, 'SUVs'));
// => { label: 'SUVs', subs: [] }
<script src="https://bundle.run/[email protected]"></script>
.as-console-wrapper {max-height: 100% !important; top: 0}
Disclaimer: I'm the author of object-scan
Solution 8 - Javascript
To increase performance for further tree manipulation is good to transform tree view into line collection view, like [obj1, obj2, obj3]. You can store parent-child object relations to easy navigate to parent/child scope.
Searching element inside collection is more efficient then find element inside tree (recursion, addition dynamic function creation, closure).
Solution 9 - Javascript
modify from Peter Olson's answer: https://stackoverflow.com/a/8085118
- can avoid string value
!obj || (typeof obj === 'string'
- can custom your key
var findObjectByKeyVal= function (obj, key, val) {
if (!obj || (typeof obj === 'string')) {
return null
}
if (obj[key] === val) {
return obj
}
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
var found = findObjectByKeyVal(obj[i], key, val)
if (found) {
return found
}
}
}
return null
}
Solution 10 - Javascript
The following snippet will iterate over nested objects. Objects within the objects. Feel free to change it to meet your requirements. Like if you want to add array support make if-else and make a function that loop through arrays ...
var p = {
"p1": "value1",
"p2": "value2",
"p3": "value3",
"p4": {
"p4": 'value 4'
}
};
/**
* Printing a nested javascript object
*/
function jsonPrinter(obj) {
for (let key in obj) {
// checking if it's nested
if (obj.hasOwnProperty(key) && (typeof obj[key] === "object")) {
jsonPrinter(obj[key])
} else {
// printing the flat attributes
console.log(key + " -> " + obj[key]);
}
}
}
jsonPrinter(p);
Solution 11 - Javascript
You can have a recursive function with a parse function built within it.
Here how it works
// recursively loops through nested object and applys parse function
function parseObjectProperties(obj, parse) {
for (var k in obj) {
if (typeof obj[k] === 'object' && obj[k] !== null) {
parseObjectProperties(obj[k], parse)
} else if (obj.hasOwnProperty(k)) {
parse(obj, k)
}
}
}
//**************
// example
var foo = {
bar:'a',
child:{
b: 'b',
grand:{
greatgrand: {
c:'c'
}
}
}
}
// just console properties
parseObjectProperties(foo, function(obj, prop) {
console.log(prop + ':' + obj[prop])
})
// add character a on every property
parseObjectProperties(foo, function(obj, prop) {
obj[prop] += 'a'
})
console.log(foo)
Solution 12 - Javascript
In typescript with object/generic way, it could be alse implemented:
export interface INestedIterator<T> {
getChildren(): T[];
}
export class NestedIterator {
private static forEach<T extends INestedIterator<T>>(obj: T, fn: ((obj: T) => void)): void {
fn(obj);
if (obj.getChildren().length) {
for (const item of obj.getChildren()) {
NestedIterator.forEach(item, fn);
};
}
}
}
than you can implement interface INestedIterator<T>
:
class SomeNestedClass implements INestedIterator<SomeNestedClass>{
items: SomeNestedClass[];
getChildren() {
return this.items;
}
}
and then just call
NestedIterator.forEach(someNesteObject, (item) => {
console.log(item);
})
if you don't want use interfaces and strongly typed classes, just remove types
export class NestedIterator {
private static forEach(obj: any, fn: ((obj: any) => void)): void {
fn(obj);
if (obj.items && obj.items.length) {
for (const item of obj.items) {
NestedIterator.forEach(item, fn);
};
}
}
}
Solution 13 - Javascript
I made a pick method like lodash pick. It is not exactly good like lodash _.pick, but you can pick any property event any nested property.
- You just have to pass your object as a first argument then an array of properties in which you want to get their value as a second argument.
for example:
let car = { name: 'BMW', meta: { model: 2018, color: 'white'};
pick(car,['name','model']) // Output will be {name: 'BMW', model: 2018}
Code :
const pick = (object, props) => {
let newObject = {};
if (isObjectEmpty(object)) return {}; // Object.keys(object).length <= 0;
for (let i = 0; i < props.length; i++) {
Object.keys(object).forEach(key => {
if (key === props[i] && object.hasOwnProperty(props[i])) {
newObject[key] = object[key];
} else if (typeof object[key] === "object") {
Object.assign(newObject, pick(object[key], [props[i]]));
}
});
}
return newObject;
};
function isObjectEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) return false;
}
return true;
}
export default pick;
and here is the link to live example with unit tests
Solution 14 - Javascript
var findObjectByLabel = function(objs, label) {
if(objs.label === label) {
return objs;
}
else{
if(objs.subs){
for(var i in objs.subs){
let found = findObjectByLabel(objs.subs[i],label)
if(found) return found
}
}
}
};
findObjectByLabel(cars, "Ford");
Solution 15 - Javascript
var findObjectByLabel = function(obj, label)
{
var foundLabel=null;
if(obj.label === label)
{
return obj;
}
for(var i in obj)
{
if(Array.isArray(obj[i])==true)
{
for(var j=0;j<obj[i].length;j++)
{
foundLabel = findObjectByLabel(obj[i], label);
}
}
else if(typeof(obj[i]) == 'object')
{
if(obj.hasOwnProperty(i))
{
foundLabel = findObjectByLabel(obj[i], label);
}
}
if(foundLabel)
{
return foundLabel;
}
}
return null;
};
var x = findObjectByLabel(cars, "Sedan");
alert(JSON.stringify(x));