Search code examples
javascriptarraysobjectrecursion

How to find the key of a value in a nested object recursively


I want to find the key of a value in a Javascript nested object with recursion.

Here is my attempt at the function. Are there more elegant ways to implement this?

const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' }

function findKey(obj, target) {
  let result = null;
  if (_.isEmpty(obj) || !_.isObject(obj)){
    return null;
  }
  if (!_.isArray(obj) && Object.keys(obj).length > 0) {
    for(let i=0; i < Object.keys(obj).length; i++){
      let key = Object.keys(obj)[i];
      let val = obj[key];
      if (val === target) {
        return key;
      }else{
        result = findKey(val, target);
      }
      if (result) {break}
    }
  }
  return result;
}
console.log(findKey(foo, 'worked'))
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

For instance is there a way to avoid having to check the value of result to then break? I feel like result should be able to bubble down the call stack until it returns at the very first function call without having to break.


Solution

  • After the few questions made above, it looks like the function should:

    • Assume the input is always an object.
    • Assume it might encounter arrays in its way.
    • Assume it must stop after meeting one value (in case multiple value exists).

    The provided input code given by the OP does not handle array cases.

    Below code is sampled to work with these sample cases:

    • Plain nested object structure.
    • Object with nested arrays of objects or elements.

    Below function accepts a second argument which is a callback to evaluate whether the element met is actually the one we're looking for. In this way, it's easier to handle more complex checks.

    The recursive approach is kept and, once the key is met, the function simply return to avoid unnecessary searchs.

    const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' };
    const fooWithArrays = {
      data: {
        data2: {
          data3: 'not here'
        },
        data4: [
          { data5: 'worked' },
          { data6: 'not me' }
        ]
      }
    };
    const fooWithExpression = {
      data: {
       data2: {
        data3: { id: 15, name: 'find me!' }
       },
       data21: {
        data25: 'not me'
       }
      }
    };
    
    const findKeyByValue = (obj, equalsExpression) => {
      // Loop key->value pairs of the input object.
      for (var [key, v] of Object.entries(obj)) {
        // if the value is an array..
        if (Array.isArray(v)) {
          // Loop the array.
          for (let i = 0; i < v.length; i++) {
            // check whether the recursive call returns a result for the nested element.
            let res = findKeyByValue(v[i], equalsExpression);
            // if so, the key was returned. Simply return.
            if (res !== null && res !== undefined) return res;
          }
        }
        // otherwise..
        else {
          // if the value is not null and not undefined.
          if (v !== null && v !== undefined) {
            // if the value is an object (typeof(null) would give object, hence the above if). 
            if (typeof(v) === 'object') {
              // check whether the value searched is an object and the match is met.
              if (equalsExpression(v)) return key;
              // if not, recursively keep searching in the object.
              let res = findKeyByValue(v, equalsExpression);
              // if the key is found, return it.
              if (res !== null && res !== undefined) return res;
            }
            else {
              // finally, value must be a primitive or something similar. Compare.
              let res = equalsExpression(v);
              // if the condition is met, return the key.
              if (res) return key;
              // else.. continue.
            }
          }
          else continue;
        }
      }
    }
    
    console.log( findKeyByValue(foo, (found) => found === 'worked') );
    console.log( findKeyByValue(fooWithArrays, (found) => found === 'worked') );
    console.log( findKeyByValue(fooWithExpression, (found) => found && found.id && found.id === 15) );