Iterate through Nested JavaScript Objects

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: []
    }
  ]
}

Answers:

Answer

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");
Answer

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') {
            iterate(obj[key])
        }
    })
}

REPL example.

Answer

Here is a dead-simple method using only 3 variables, only 9 lines of code, and no recursion.

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:

  1. the code changes the value of input parameters (arguments) [lines 2 & 5],
  2. the code calls Array.prototype.push and Array.prototype.pop on every single item [lines 5 & 8],
  3. the code only does a pointer-comparison for the constructor which does not work on out-of-window objects [line 7],
  4. the code duplicates the array returned from Object.values [line 8],
  5. the code does not localize window.Object or window.Object.values [line 9],
  6. 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 does 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.

Answer

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;
}
Answer

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');

Answer

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;
}
Answer

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).

Answer

modify from Peter Olson's answer: https://stackoverflow.com/a/8085118

  1. can avoid string value !obj || (typeof obj === 'string'
  2. 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
}
Answer

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);

Answer

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)

Answer

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

Answer
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));
Answer
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");

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.