最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Javascript object recursion to find an item at the deepest level - Stack Overflow

programmeradmin0浏览0评论

I'm trying to recursively search an object that contains strings, arrays, and other objects to find an item (match a value) at the deepest level however I'm always getting undefined as the return result. I can see through some console logging that I am finding the item but it gets overwritten. Any idea where I'm going wrong?

  var theCobWeb = {
    biggestWeb: {
      item: "b",
      biggerWeb: {
        items: ["glasses", "paperclip", "bubblegum"],
        smallerWeb: {
          item: "toothbrush",
          tinyWeb: {
            items: ["toenails", "lint", "wrapper", "homework"]
          }
        }
      },
      otherBigWeb: {
        item: "headphones"
      }
    }
  };

  function findItem (item, obj) {
  var foundItem;
  for (var key in obj) {
    if (obj[key] === item) {
      foundItem = obj;
    } else if (Array.isArray(obj[key]) && obj[key].includes(item)) {
      foundItem = obj;
    } else if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
      findItem(item, obj[key]);
    } 
  }
      return foundItem;
}

var foundIt = findItem('glasses', theCobWeb);
console.log('The item is here: ' + foundIt); // The item is here: undefined 

Edit: cleaned up the code a bit based on feedback below.

I'm trying to recursively search an object that contains strings, arrays, and other objects to find an item (match a value) at the deepest level however I'm always getting undefined as the return result. I can see through some console logging that I am finding the item but it gets overwritten. Any idea where I'm going wrong?

  var theCobWeb = {
    biggestWeb: {
      item: "b",
      biggerWeb: {
        items: ["glasses", "paperclip", "bubblegum"],
        smallerWeb: {
          item: "toothbrush",
          tinyWeb: {
            items: ["toenails", "lint", "wrapper", "homework"]
          }
        }
      },
      otherBigWeb: {
        item: "headphones"
      }
    }
  };

  function findItem (item, obj) {
  var foundItem;
  for (var key in obj) {
    if (obj[key] === item) {
      foundItem = obj;
    } else if (Array.isArray(obj[key]) && obj[key].includes(item)) {
      foundItem = obj;
    } else if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
      findItem(item, obj[key]);
    } 
  }
      return foundItem;
}

var foundIt = findItem('glasses', theCobWeb);
console.log('The item is here: ' + foundIt); // The item is here: undefined 

Edit: cleaned up the code a bit based on feedback below.

Share Improve this question edited Mar 13, 2017 at 0:00 redeux asked Mar 12, 2017 at 22:35 redeuxredeux 572 silver badges9 bronze badges 0
Add a ment  | 

3 Answers 3

Reset to default 3
function findItem (item, obj) {
  for (var key in obj) {
    if (obj[key] === item) { // if the item is a property of the object
      return obj;            // return the object and stop further searching
    } else if (Array.isArray(obj[key]) && obj[key].includes(item)) { // if the item is inside an array property of the object
      return obj;            // return the object and stop the search
    } else if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { // if the property is another object
      var res = findItem(item, obj[key]); // get the result of the search in that sub object
      if(res) return res; // return the result if the search was successful, otherwise don't return and move on to the next property
    } 
  }
  return null; // return null or any default value you want if the search is unsuccessful (must be falsy to work)
}

Note 1: Array.isArray and Array.prototype.includes already returning booleans so there is no need to check them against booleans.

Note 2: You can flip the value of a boolean using the NOT operator (!).

Note3: You have to return the result (if found) immediately after it is found so you won't waste time looking for something you already have.

Note4: The return result of the search will be an object (if found) and since objects are passed by reference not by value, changing the properties of that object will change the properties of the original object too.

Edit: Find the deepest object:

If you want to find the deepest object, you'll have to go throug every object and sub-object in the object obj and everytime you have to store the object and it's depth (if the depth of the result is bigger than the previous result of course). Here is the code with some ments (I used an internal function _find that actually get called on all the objects):

function findItem (item, obj) {
    var found = null;              // the result (initialized to the default return value null)
    var depth = -1;                // the depth of the current found element (initialized to -1 so any found element could beat this one) (matched elements will not be assigned to found unless they are deeper than this depth)

    function _find(obj, d) {       // a function that take an object (obj) and on which depth it is (d)
        for (var key in obj) {     // for each ...
            // first call _find on sub-objects (pass a depth of d + 1 as we are going to a one deep bellow)
            if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { 
                _find(obj[key], d + 1);
            }
            // then check if this object actually contain the item (we still at the depth d)
            else if (obj[key] === item || (Array.isArray(obj[key]) && obj[key].includes(item))) {
                // if we found something and the depth of this object is deeper than the previously found element
                if(d > depth) {
                    depth = d;     // then assign the new depth
                    found = obj;   // and assign the new result
                }
            }
        }
    }
    _find(obj, 0);                 // start the party by calling _find on the object obj passed to findItem with a depth of 0

    // at this point found is either the initial value (null) means nothing is found or it is an object (the deepest one)
    return found;
}

"I'm trying to recursively search an object that contains strings, arrays, and other objects to find an item (match a value) at the deepest level however I'm always getting undefined as the return result."

var foundIt = findItem('glasses', theCobWeb);
console.log('The item is here: ' + foundIt); // The item is here: undefined 

"The item is here ..." – where?

Well what exactly do you want as a return value? Should it just say "glasses" when it's all done? In my opinion, that's kind of pointless – fundamentally it's no better than just returning true or false.

I wrote this function a while ago because I needed to search a heap of data but also know specifically where it matched. I'd probably revise this a little bit now (or at least include type annotations), but it works as-is, so here you go.

// helpers
const keys = Object.keys
const isObject = x=> Object(x) === x
const isArray = Array.isArray
const rest = ([x,...xs])=> xs

// findDeep
const findDeep = (f,x) => {
  let make = (x,ks)=> ({node: x, keys: ks || keys(x)})
  let processNode = (parents, path, {node, keys:[k,...ks]})=> {
    if (k === undefined)
      return loop(parents, rest(path))
    else if (isArray(node[k]) || isObject(node[k]))
      return loop([make(node[k]), make(node, ks), ...parents], [k, ...path])
    else if (f(node[k], k))
      return {parents, path: [k,...path], node}
    else
      return loop([{node, keys: ks}, ...parents], path)
  }
  let loop = ([node,...parents], path) => {
    if (node === undefined)
      return {parents: [], path: [], node: undefined}
    else
      return processNode(parents, path, node)
  }
  return loop([make(x)], [])
}

// your sample data
var theCobWeb = {biggestWeb: {item: "b",biggerWeb: {items: ["glasses", "paperclip", "bubblegum"],smallerWeb: {item: "toothbrush",tinyWeb: {items: ["toenails", "lint", "wrapper", "homework"]}}},otherBigWeb: {item: "headphones"}}};

// find path returns {parents, path, node}
let {path, node} = findDeep((value,key)=> value === "glasses", theCobWeb)

// path to get to the item, note it is in reverse order
console.log(path) // => [0, 'items', 'biggerWeb', 'biggestWeb']

// entire matched node
console.log(node) // => ['glasses', 'paperclip', 'bubblegum']

The basic intuition here is node[path[0]] === searchTerm


Complete path to the matched query

We get the entire key path to the matched data. This is useful because we know exactly where it is based on the root of our search. To verify the path is correct, see this example

const lookup = ([p,...path], x) =>
  (p === undefined) ? x : lookup(path,x)[p]
lookup([0, 'items', 'biggerWeb', 'biggestWeb'], theCobWeb) // => 'glasses'

Unmatched query

Note if we search for something that is not found, node will be undefined

let {path, node} = findDeep((value,key)=> value === "sonic the hog", theCobWeb)
console.log(path) // => []
console.log(node) // => undefined

Searching for a specific key/value pair

The search function receives a value and key argument. Use them as you wish

let {path, node} = findDeep((value,key)=> key === 'item' && value === 'toothbrush', theCobWeb)
console.log(path) // => [ 'item', 'smallerWeb', 'biggerWeb', 'biggestWeb' ]
console.log(node) // => { item: 'toothbrush', tinyWeb: { items: [ 'toenails', 'lint', 'wrapper', 'homework' ] } }

Short circuit – 150cc

Oh and because I spoil you, findDeep will give an early return as soon as the first match is found. It won't waste putation cycles and continue iterating through your pile of data after it knows the answer. This is a good thing.


Go exploring

Have courage, be adventurous. The findDeep function above also gives a parents property on returned object. It's probably useful to you in some ways, but it's a little more plicated to explain and not really critical for answering the question. For the sake of keeping this answer simplified, I'll just mention it's there.

That's because the recursive call doesn't assign the return to the variable. And you should check the return from the recursive call and return if true or break from the for loop if you have other logic after it.

 function findItem(item, obj) {
 for (var key in obj) {
     if (obj[key] === item) {
         return obj;
     } else if (Array.isArray(obj[key]) === true && obj[key].includes(item) === true) {
         return obj;
     } else if (typeof obj[key] === 'object' && Array.isArray(obj[key]) === false) {
         var foundItem = findItem(item, obj[key]);
         if(foundItem) 
          return foundItem;
     }
 }
发布评论

评论列表(0)

  1. 暂无评论