通过深度嵌套的 object/array 递归搜索并在不知道键的情况下收集包含指定字符串的值

Search recursively through a deeply nested object/array and collect values that include a specified string without knowing the key(s)

假设我有一个深度嵌套的对象或数组,从 API 返回,它可能如下所示:

const array = [
  {
    key1: 'dog',
    key2: 'cat'
  },
  {
    key1: 'dog',
    key2: 'cat'
    key3: [
      {
        keyA: 'mouse',
        keyB: 'https://myURL.com/path/to/file/1'
        keyC: [
          {
            keyA: 'tiger',
            keyB: 'https://myURL.com/path/to/file/2'
          }
        ]
      }
    ]
  },
  {
    key4: 'dog',
    key5: 'https://myURL.com/path/to/file/3'
  }
]

我想递归遍历 object/array 并构建一个包含所有字符串值的数组并包含子字符串 https://myURL.com,并构建一个包含匹配项的数组,如下所示:

let matches = [
   'https://myURL.com/path/to/file/1',
   'https://myURL.com/path/to/file/2',
   'https://myURL.com/path/to/file/3'
]

重要的一点是,我事先并不知道相关的密钥是什么。 URL 的值可能位于任何键上。因此,在此示例中,我不能简单地查找 keyBkey5 - 我必须测试每个 key:value 对以查找包含 URL 字符串的值。

问题

如何创建递归函数来搜索对象中包含 .includes() 字符串 'https://myURL.com' 的值?很高兴使用 lodash 方法,所以请随时提交依赖于 lodash 的解决方案。

上下文

数据来自 CMS API,我将使用 URLS 的数组(在本例中为上面的 matches)到 add assets to cache with Cache API,异步在后台,用于 PWA 的离线功能。

非常感谢。

此递归函数查找与谓词匹配的叶节点:

const getMatches = (pred) => (obj) =>
  obj == null ?
    [] 
  : Array .isArray (obj)
    ? obj .flatMap (getMatches (pred))
  : typeof obj == 'object'
    ? Object .values (obj) .flatMap (getMatches (pred))
  : pred (obj)
    ? obj
  : []


const array = [{key1: 'dog', key2: 'cat'},{key1: 'dog', key2: 'cat', key3: [{keyA: 'mouse', keyB: 'https://myURL.com/path/to/file/1', keyC: [{keyA: 'tiger', keyB: 'https://myURL.com/path/to/file/2'}]}]}, {key4: 'dog',key5: 'https://myURL.com/path/to/file/3'}]

const check = val => typeof val == 'string' && val .includes ('https://myURL.com')

console .log (
  getMatches (check) (array)
)

我们可以修改它以也包括非叶节点。但这似乎是您在这种情况下所需要的。

我们只需将谓词传递给它来测试我们的节点,然后将要测试的对象或数组传递给结果函数。它将 return 匹配的节点。

如果愿意,您可以通过不直接传递对象来保存中间函数:

const getMyUrls = getMatches (check)
// ... later
const urls = getMyUrls (someObject);

更新

谢谢你的回答让我意识到我的功能可以进一步简化。这是它的更好版本:

const getMatches = (pred) => (obj) =>
  Object (obj) === obj
    ? Object .values (obj) .flatMap (getMatches (pred))
  : pred (obj)
    ? obj
  : []

这是使用 valuesfilter 生成器的通用方法 -

const values = function* (t)
{ if (Object(t) === t)
    for (const v of Object.values(t))
      yield* values(v)
  else
    yield t
}

const filter = function* (test, t)
{ for (const v of values(t))
    if (test(v))
      yield v
}

我们现在可以使用 filter -

编写一个简单的 query
const myArray =
  [ ... ]

const query =
  filter
    ( v =>
        String(v) === v
          && v.startsWith("https://myURL.com")
    , myArray
    )

const result =
  Array.from(query)

console.log(result)
// [ 'https://myURL.com/path/to/file/1'
// , 'https://myURL.com/path/to/file/2'
// , 'https://myURL.com/path/to/file/3'
// ]

使用像 filter 这样的泛型,我们可以编写像 searchByString -

这样的特殊变体
const searchByString = (test, t) =>
  filter                                     // <-- specialisation
    ( v => String(v) === v && test(v)        // <-- auto reject non-strings
    , t
    )

现在调用站点的查询更清晰了 -

const myArray =
  [ ... ]

const query =
  searchByString                              // <-- specialised
    ( v => v.startsWith("https://myURL.com")  // <-- simplified filter
    , myArray
    )

const result =
  Array.from(query)

console.log(result)
// [ 'https://myURL.com/path/to/file/1'
// , 'https://myURL.com/path/to/file/2'
// , 'https://myURL.com/path/to/file/3'
// ]

展开下面的代码片段以在浏览器中验证结果 -

const myArray =
  [{key1:'dog',key2:'cat'},{key1:'dog',key2:'cat',key3:[{keyA:'mouse',keyB:'https://myURL.com/path/to/file/1',keyC:[{keyA:'tiger',keyB:'https://myURL.com/path/to/file/2'}]}]},{key4:'dog',key5:'https://myURL.com/path/to/file/3'}]
  
const values = function* (t)
{ if (Object(t) === t)
    for (const v of Object.values(t))
      yield* values(v)
  else
    yield t
}

const filter = function* (test, t)
{ for (const v of values(t))
    if (test(v))
      yield v
}

const searchByString = (test, t) =>
  filter
    ( v =>
        String(v) === v && test(v)
    , t
    )

const query =
  searchByString
    ( v => v.startsWith("https://myURL.com")
    , myArray
    )

const result =
  Array.from(query)

console.log(result)
// [ 'https://myURL.com/path/to/file/1'
// , 'https://myURL.com/path/to/file/2'
// , 'https://myURL.com/path/to/file/3'
// ]