在对象嵌套数组中查找对象的路径

Find path to object in object nested array

我有一个对象,其参数包含对象数组。我收到 1 个对象 ID,我需要在整个混乱中找到它的位置。通过程序化编程,我可以使用它:

const opportunitiesById =  {
  1: [
    { id: 1, name: 'offer 1' },
    { id: 2, name: 'offer 1' }
  ],
  2: [
    { id: 3, name: 'offer 1' },
    { id: 4, name: 'offer 1' }
  ],
  3: [
    { id: 5, name: 'offer 1' },
    { id: 6, name: 'offer 1' }
  ]
};

const findObjectIdByOfferId = (offerId) => {
  let opportunityId;
  let offerPosition;
  const opportunities = Object.keys(opportunitiesById);

  opportunities.forEach(opportunity => {
    const offers = opportunitiesById[opportunity];

    offers.forEach((offer, index) => {
      if (offer.id === offerId) {
        opportunityId = Number(opportunity);
        offerPosition = index;
      }
    })
  });

return { offerPosition, opportunityId };
}

console.log(findObjectIdByOfferId(6)); // returns { offerPosition: 1, opportunityId: 3 }

但是这并不漂亮,我想以实用的方式做到这一点。 我查看了 Ramda,当我查看单个报价数组时可以找到报价,但我找不到查看整个对象的方法 => 每个数组都可以找到我报价的路径.

R.findIndex(R.propEq('id', offerId))(opportunitiesById[1]);

我需要知道路径的原因是因为我需要用新数据修改该报价并将其更新回原处。

感谢您的帮助

我会把你的对象变成一对。

所以例如转换这个:

{ 1: [{id:10}, {id:20}],
  2: [{id:11}, {id:21}] }

进入那个:

[ [1, [{id:10}, {id:20}]],
  [2, [{id:11}, {id:21}]] ]

然后您可以遍历该数组并将每个报价数组缩减为您要查找的报价的索引。假设您正在寻找报价 #21,上面的数组将变为:

[ [1, -1],
  [2,  1] ]

然后你 return 第一个元组第二个元素不等于 -1:

[2, 1]

以下是我建议的做法:

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' },
       { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' },
       { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' },
       { id: 22, name: 'offer 6' } ]
};

const findOfferPath = (id, offers) =>
  pipe(
    toPairs,
    transduce(
      compose(
        map(over(lensIndex(1), findIndex(propEq('id', id)))),
        reject(pathEq([1], -1)),
        take(1)),
      concat,
      []))
    (offers);


console.log(findOfferPath(21, opportunitiesById));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {pipe, transduce, compose, map, over, lensIndex, findIndex, propEq, reject, pathEq, take, concat, toPairs} = R;</script>

然后您可以按照您认为合适的方式修改报价:

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' },
       { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' },
       { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' },
       { id: 22, name: 'offer 6' } ]
};

const updateOffer = (path, update, offers) =>
  over(lensPath(path), assoc('name', update), offers);

console.log(updateOffer(["2", 1], '', opportunitiesById));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {over, lensPath, assoc} = R;</script>

可以使用许多小函数将它拼凑起来,但我想向您展示如何以更直接的方式编码您的意图。该程序还有一个额外的好处,它会立即 return。也就是说,在找到匹配项后,它将 而不是 继续搜索额外的 key/value 对。

这里有一种使用相互递归的方法。首先我们写 findPath -

const identity = x =>
  x

const findPath =
  ( f = identity
  , o = {}
  , path = []
  ) =>
    Object (o) === o
      ? f (o) === true
        ? path
        : findPath1 (f, Object .entries (o), path)
      : undefined

如果输入是对象,我们将其传递给用户的搜索功能f。如果用户的搜索功能 returns true,已找到匹配项,我们 return path。如果不匹配,我们将使用辅助函数搜索对象的每个 key/value 对。否则,如果输入 不是 对象,则没有匹配项,也没有任何内容可供搜索,因此 return undefined。我们编写助手,findPath1 -

const None =
  Symbol ()

const findPath1 =
  ( f = identity
  , [ [ k, v ] = [ None, None ], ...more ]
  , path = []
  ) =>
    k === None
      ? undefined
      : findPath (f, v, [ ...path, k ])
        || findPath1 (f, more, path)

如果 key/value 对已用完,则没有任何内容可搜索,因此 return undefined。否则我们有一个键 k 和一个值 v;将 k 附加到路径并递归搜索 v 以进行匹配。如果没有匹配,递归搜索剩余的key/values,more,使用相同的path.

注意每个函数的简单性。除了 assemble a path 到匹配对象的绝对最小步数之外,没有任何事情发生。你可以这样使用它 -

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

findPath (offer => offer.id === 6, opportunitiesById)
// [ '3', '1' ]

路径 returned 引导我们找到我们想要找到的对象 -

opportunitiesById['3']['1']
// { id: 6, name: 'offer 1' }

我们可以专门化 findPath 来制作一个直观的 findByOfferId 函数 -

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

findByOfferId (3, opportunitiesById)
// [ '2', '0' ]

opportunitiesById['2']['0']
// { id: 3, name: 'offer 1' }

Array.prototype.find 一样,如果从未找到匹配,则 returns undefined -

findByOfferId (99, opportunitiesById)
// undefined

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

const identity = x =>
  x

const None =
  Symbol ()

const findPath1 =
  ( f = identity
  , [ [ k, v ] = [ None, None ], ...more ]
  , path = []
  ) =>
    k === None
      ? undefined
      : findPath (f, v, [ ...path, k ])
        || findPath1 (f, more, path)

const findPath =
  ( f = identity
  , o = {}
  , path = []
  ) =>
    Object (o) === o
      ? f (o) === true
        ? path
        : findPath1 (f, Object .entries (o), path)
      : undefined

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]

console .log (opportunitiesById['2']['0'])
// { id: 3, name: 'offer 1' }

console .log (findByOfferId (99, opportunitiesById))
// undefined


在这个 related Q&A 中,我演示了一个递归搜索函数,该函数 return 是匹配的对象,而不是匹配的路径。还有其他有用的花絮,所以我建议你看一看。


Scott 的回答启发我尝试使用生成器实现。我们从 findPathGen -

开始
const identity = x =>
  x

const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
    if (f (o) === true)
      yield path
    else
      yield* findPathGen1 (f, Object .entries (o), path)
}

并且像我们上次一样使用相互递归,我们调用 helper findPathGen1 -

const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
    yield* findPathGen (f, v, [ ...path, k ])
}

最后,我们可以实现 findPath 和特化 findByOfferId -

const first = ([ a ] = []) =>
  a

const findPath = (f = identity, o = {}) =>
  first (findPathGen (f, o))

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

效果一样-

findPath (offer => offer.id === 3, opportunitiesById)
// [ '2', '0' ]

findPath (offer => offer.id === 99, opportunitiesById)
// undefined

findByOfferId (3, opportunitiesById)
// [ '2', '0' ]

findByOfferId (99, opportunitiesById)
// undefined

作为奖励,我们可以使用 Array.from -

轻松实现 findAllPaths
const findAllPaths = (f = identity, o = {}) =>
  Array .from (findPathGen (f, o))

findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById)
// [ [ '2', '0' ], [ '3', '1' ] ]

通过展开下面的代码段来验证结果

const identity = x =>
  x

const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
    if (f (o) === true)
      yield path
    else
      yield* findPathGen1 (f, Object .entries (o), path)
}

const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
    yield* findPathGen (f, v, [ ...path, k ])
}

const first = ([ a ] = []) =>
  a

const findPath = (f = identity, o = {}) =>
  first (findPathGen (f, o))


const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]

console .log (findByOfferId (99, opportunitiesById))
// undefined

// --------------------------------------------------
const findAllPaths = (f = identity, o = {}) =>
  Array .from (findPathGen (f, o))

console .log (findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById))
// [ [ '2', '0' ], [ '3', '1' ] ]

这是另一种方法:

我们从这个生成器函数开始:

function * getPaths(o, p = []) {
  yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, k])
} 

可用于查找对象中的所有路径:

const obj = {a: {x: 1, y: 3}, b: {c: 2, d: {x: 3}, e: {f: {x: 5, g: {x: 3}}}}}

;[...getPaths(obj)]
//~> [[], ["a"], ["a", "x"], ["a", "y"], ["b"], ["b", "c"], ["b", "d"], 
//    ["b", "d", "x"], ["b", "e"], ["b", "e", "f"], ["b", "e", "f", "x"], 
//    ["b", "e", "f", "g"], ["b", "e", "f", "g", "x"]]

然后,使用这个小辅助函数:

const path = (ps, o) => ps.reduce((o, p) => o[p] || {}, o)

我们可以写

const findPath = (predicate, o) =>
  [...getPaths(o)] .find (p => predicate (path (p, o) ) )

我们可以这样称呼

console.log(
  findPath (a => a.x == 3, obj)
) //~> ["b","d"]

然后我们可以使用这些函数来编写您的函数的简单版本:

const findByOfferId = (id, data) =>
  findPath (o => o.id === id, data)

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' }, { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' }, { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' }, { id: 22, name: 'offer 6' } ]
}

console.log(
  findByOfferId (22, opportunitiesById)
) //~> ["3", "1"]

console.log(
  findByOfferId (42, opportunitiesById)
) //~> undefined

扩展它以获取值满足谓词的所有路径是微不足道的,只需将 find 替换为 filter:

const findAllPaths = (predicate, o) =>
  [...getPaths(o)] .filter (p => predicate (path(p, o) ) )

console.log(
  findAllPaths (a => a.x == 3, obj)
) //=> [["b","d"],["b","e","f","g"]]

不过,这一切都令人担忧。尽管 findPath 只需要找到第一个匹配项,并且即使 getPaths 是一个生成器并且因此是惰性的,我们还是用 [...getPaths(o)] 强制它的完整 运行。所以可能值得使用这个更丑陋、更命令的版本:

const findPath = (predicate, o) => {
  let it = getPaths(o)
  let res = it.next()
  while (!res.done) {
    if (predicate (path (res.value, o) ) )
      return res.value
    res = it.next()
  }
}

这是整体的样子:

function * getPaths(o, p = []) {
  yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, k])
}

const path = (ps, o) => ps.reduce ((o, p) => o[p] || {}, o)

   
// const findPath = (pred, o) =>
//   [...getPaths(o)] .find (p => pred (path (p, o) ) )


const findPath = (predicate, o) => {
  let it = getPaths(o)
  let res = it.next()
  while (!res.done) {
    if (predicate (path (res.value, o) ) )
      return res.value
    res = it.next()
  }
}

const obj = {a: {x: 1, y: 3}, b: {c: 2, d: {x: 3}, e: {f: {x: 5, g: {x: 3}}}}}

console.log(
  findPath (a => a.x == 3, obj)
) //~> ["b","d"]

const findAllPaths = (pred, o) =>
  [...getPaths(o)] .filter (p => pred (path(p, o) ) )

console.log(
  findAllPaths (a => a.x == 3, obj)
) //~> [["b","d"],["b","e","f","g"]]


const findByOfferId = (id, data) =>
  findPath (o => o.id === id, data)

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' }, { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' }, { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' }, { id: 22, name: 'offer 6' } ]
}

console.log(
  findByOfferId (22, opportunitiesById)
) //~> ["3", "1"]

console.log(
  findByOfferId (42, opportunitiesById)
) //~> undefined


另一个简短的说明:生成路径的顺序只是一种可能性。如果想从pre-order to post-order改过来,可以把getPaths中的yield p行从第一行移到最后一行。


最后,您问及使用函数式技术来实现这一点,并提到了 Ramda。正如 customcommander 的解决方案所示,您 可以 使用 Ramda 执行此操作。来自 user633183 的(一如既往的出色)回答表明,主要使用功能技术可以做到这一点。

我仍然觉得这是一种更简单的方法。感谢 customcommander 找到了 Ramda 版本,因为 Ramda 并不是特别适合 well-suited 递归任务,但是对于必须访问像 JS 对象这样的递归结构的节点的东西,显然的方法仍然是使用递归算法.我是 Ramda 的作者之一,我什至 尝试 了解该解决方案的工作原理。

更新

user633183 指出这样会更简单,而且还是偷懒:

const findPath = (predicate, o) => {
  for (const p of getPaths(o)) 
    if (predicate (path (p, o)) ) 
      return p
}