DEV Community

Discussion on: Search through a JSON object using JavaScript

Collapse
 
qm3ster profile image
Mihail Malo • Edited

Otherwise, I'd use an inner recursive function so that I can do option parsing once, and then close over one array I will always append to, to avoid allocating intermediate arrays:

/**
 * searches deep into an object recursively...
 * @param {Object} obj object to be searched
 * @param {any} searchValue the value/key to search for
 * @param {Object} [options]
 * @param {boolean} options.[searchKeys] whether to search object keys as well as values. Defaults to `true` if `serchValue` is a string, `false` otherwise.
 * @param {number} options.[maxDepth=20] maximum recursion depth (to avoid "Maximum call stack size exceeded")
 * @returns {string[]} Paths on the object to the matching results
 */
const findPaths = (
  obj,
  searchValue,
  { searchKeys = typeof searchValue === "string", maxDepth = 20 } = {}
) => {
  const paths = []
  const notObject = typeof searchValue !== "object"
  const gvpio = (obj, maxDepth, prefix) => {
    if (!maxDepth) return

    for (const [curr, currElem] of Object.entries(obj)) {
      if (searchKeys && curr === searchValue) {
        // To search for property name too ...
        paths.push(prefix + curr)
      }

      if (typeof currElem === "object") {
        // object is "object" and "array" is also in the eyes of "typeof"
        // search again :D
        gvpio(currElem, maxDepth - 1, prefix + curr + "/")
        if (notObject) continue
      }
      // it's something else... probably the value we are looking for
      // compares with "searchValue"
      if (currElem === searchValue) {
        // return index AND/OR property name
        paths.push(prefix + curr)
      }
    }
  }
  gvpio(obj, maxDepth, "")
  return paths
}

Here, I'm also using an options object for convenience, with a smart default for searchKeys, since object keys are always strings, even in arrays:

console.log(findPaths([[[]],[[]]],0)) // []
console.log(findPaths([[[]],[[]]],'0')) // [ '0', '0/0', '1/0' ]

I'm also building the path string with no duplication. This includes eliminating the inner loop, since we never return an intermediary array back up. The reason prefix + curr occurs in 3 places is because in a real search, on most keys none of those conditions will happen, and almost never will two happen together.

Collapse
 
killants profile image
killants • Edited

Not used to see many arrow function (without being small ones, in array.map for example) so i got a bit confused with the parameter line :
{ searchKeys = typeof searchValue === "string", maxDepth = 20 } = {}

Would you mind to explain in a few words why it is there? :)

Thank you in advance.

Thread Thread
 
qm3ster profile image
Mihail Malo

Sure, this doesn't depend on it being an arrow function, can be in a function just as well.

This is the equivalent function:

function fn(arg) {
  if (arg === undefined) arg = {}
  let searchKeys = arg.searchKeys
  if (searchKeys === undefined) searchKeys = typeof searchValue === "string"
  let maxDepth = arg.maxDepth
  if (maxDepth === undefined) maxDepth = 20
  /* ... */
}

First, we replace the point access with destructuring:

function fn(arg) {
  if (arg === undefined) arg = {}
  let { searchKeys, maxDepth } = arg
  if (searchKeys === undefined) searchKeys = typeof searchValue === "string"
  if (maxDepth === undefined) maxDepth = 20
  /* ... */
}

Next, we replace the conditional expressions for defaults with defaults in destructuring AND default in parameter:

function fn(arg = {}) {
  const { // we can use const now because we won't be reassigning
    searchKeys = typeof searchValue === "string",
    maxDepth = 20
  } = arg
  /* ... */
}

Finally, to avoid having to think of a name for the short-lived arg binding, we can just YEET it right into the parameter list:

function fn({
  searchKeys = typeof searchValue === "string",
  maxDepth = 20
} = {}) {
  /* ... */
}

We could write it like this:

function fn(
  { 
    searchKeys = typeof searchValue === "string",
    maxDepth = 20
  } = {
    searchKeys: typeof searchValue === "string",
    maxDepth: 20
  }
) {
  /* ... */
}

But that's just more verbose. Destructuring an empty object (if undefined is provided) is perfectly adequate, and gives defaults for all keys.

function fn(
  searchValue, // this is the v below that we are depending on, it has to come first.
  { searchKeys = typeof searchValue === "string", maxDepth = 20 } = {}
) {
  /* ... */
}
Collapse
 
qm3ster profile image
Mihail Malo • Edited

Finally, at the cost of a tiny bit of memory, you can keep a Set of visited objects so you can prevent infinite recursion without a counter.

Before:

const obj = {
  a: 0,
  b: 1,
  c: [[]]
}
const obj2 = {t:obj}
obj.c[0].push(obj2, 't')
console.log(findPaths(obj,"t"));
[ 'c/0/0/t',
  'c/0/0/t/c/0/0/t',
  'c/0/0/t/c/0/0/t/c/0/0/t',
  'c/0/0/t/c/0/0/t/c/0/0/t/c/0/0/t',
  'c/0/0/t/c/0/0/t/c/0/0/t/c/0/0/t/c/0/0/t',
  'c/0/0/t/c/0/0/t/c/0/0/t/c/0/0/t/c/0/1',
  'c/0/0/t/c/0/0/t/c/0/0/t/c/0/1',
  'c/0/0/t/c/0/0/t/c/0/1',
  'c/0/0/t/c/0/1',
  'c/0/1' ]

After:

Output:

[ 'c/0/0/t', 'c/0/1' ]

Code:

/**
 * searches deep into an object recursively...
 * @param {Object} obj object to be searched
 * @param {any} searchValue the value/key to search for
 * @param {boolean} [searchKeys] whether to search object keys as well as values. Defaults to `true` if `serchValue` is a string, `false` otherwise.
 * @returns {string[]} Paths on the object to the matching results
 */
const findPaths = (
  obj,
  searchValue,
  searchKeys = typeof searchValue === "string"
) => {
  const paths = []
  const visited = new Set()
  const notObject = typeof searchValue !== "object"
  const gvpio = (obj, prefix) => {
    for (const [curr, currElem] of Object.entries(obj)) {
      if (searchKeys && curr === searchValue) {
        paths.push(prefix + curr)
      }

      if (typeof currElem === "object") {
        if (visited.has(currElem)) continue
        visited.add(currElem)
        gvpio(currElem, prefix + curr + "/")
        if (notObject) continue
      }
      if (currElem === searchValue) {
        paths.push(prefix + curr)
      }
    }
  }
  gvpio(obj, "")
  return paths
}

Disclaimer:

Be careful though, it won't always include the shortest path!

const obj = {
  t: "t"
}
const obj2 = {
  a: [{ a: [{ a: [{ obj }] }] }],
  b: obj
}
console.log(findPaths(obj2, "t"));
[ 'a/0/a/0/a/0/obj/t', 'a/0/a/0/a/0/obj/t' ] // :(