TypeOfNaN

How to Find an Object Deeply and Do Something with the Result in JavaScript

Nick Scialli
March 13, 2021

Sometimes we’re in the unenviable position of having to search an object deeply in JavaScript. How can we do this and take some action with the result?

We can take advantage of recursion and callback functions to make this work out pretty well for us.

A Sample Problem

Let’s say we have the following object and we’d like to take some action with the object corresponding to id = 7.

const obj = {
  some: {
    props: {
      drilling: {
        pretty: {
          deep: [
            {
              id: 7,
              name: 'Daffodil',
            },
          ],
        },
      },
    },
    more: {
      props: {
        leading: {
          to: 'nowhere',
        },
      },
    },
  },
};

Our sample problem has one important constrant: the current path of the desired object, obj.some.props.drilling.pretty.deep[0] is not guaranteed to always be the path. We need a robust way to dive down any number of levels deep and find this object.

The Approach

We will create a recursive function that takes three arguments:

  • the object we’re traversing
  • a matcher function that will evalutate to true when we have found the correct object
  • a callback function that will handle the object once we have found it.
function deepFindCallback(obj, matcher, cb) {
  // Magic here
}

Inside the deepFindCallback function, we will do the following:

  • call the matcher function on the object to see if we have found our match
  • if we have found our match, call the callback function cb on it
  • if we have not found our match, recursively call deepFindCallback on all children of the current object that are also objects.
function deepFindCallback(obj, matcher, cb) {
  // Call the matcher function
  // If match, call the callback
  // If not match, recursively call deepFindCallback
}

Filling Out the Function

We can fill our function out function in only about 10 lines or so based on our approach.

function deepFindCallback(obj, matcher, cb) {
  // Call the matcher function
  if (matcher(obj)) {
    // If match, call the callback
    cb(obj);
  }
  // If not match, recursively call deepFindCallback
  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      deepFindCallback(obj[key], matcher, cb);
    }
  }
}

Let’s try it out! Our example states that we want to find the object associated with id = 7, so we can use the following matcher function:

const matcher = (obj) => obj.id === 7;

For simplicity, our callback function is going to console.log the name associated with this id.

const cb = (obj) => {
  console.log(obj.name);
};

All together, this is what we get:

const obj = {
  some: {
    props: {
      drilling: {
        pretty: {
          deep: [
            {
              id: 7,
              name: 'Daffodil',
            },
          ],
        },
      },
    },
    more: {
      props: {
        leading: {
          to: 'nowhere',
        },
      },
    },
  },
};

function deepFindCallback(obj, matcher, cb) {
  if (matcher(obj)) {
    cb(obj);
  }
  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      deepFindCallback(obj[key], matcher, cb);
    }
  }
}

const matcher = (obj) => obj.id === 7;

const cb = (obj) => {
  console.log(obj.name);
};

deepFindCallback(obj, matcher, cb);
// "Daffodil"

As expected, we log "Daffodil", which is the name associated with the object that has id = 7.

Stopping Early

In the above example, our recursive function isn’t going to stop when it finds the object we’re looking for. This could be desired if we’re looking for multiple occurrences; however, we’re often only looking for one occurrence. If that’s the case, we can adjust our function to contain some context about whether the object was found.

function deepFindCallback(obj, matcher, cb) {
  let found = false;
  finder(obj, matcher, cb);
  function finder(obj, matcher, cb) {
    if (found) {
      return;
    }
    if (matcher(obj)) {
      cb(obj);
      found = true;
      return;
    }
    for (let key in obj) {
      if (typeof obj[key] === 'object') {
        finder(obj[key], matcher, cb);
      }
    }
  }
}

In this case, we add a found variable and then a new function inside our deepFindCallback. The new function is created because we need found to live outside the scope of our recursive function. Within the recursive function we bail out if our object has been found, attempting to perform as little recursion as necessary.

Recursion and Callbacks Are Your Friends

I have found that recursion and callback functions in JavaScript tend to be a powerful combination. In this case, the combo helped us dive arbitrarily deeply down an object and take pretty much any action that we wanted on the found object.

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli