【问题标题】:How to loop trough a large object that has child objects and arrays?如何遍历具有子对象和数组的大对象?
【发布时间】:2021-01-15 23:28:31
【问题描述】:

我正在开发一个需要实现简单搜索功能的应用程序,所以我有这个带有子对象和数组的大对象。通常我会像这样访问该对象中的数据:

list[id][day][show].title

但现在我需要检查该标题是否等于某个输入值,所以我创建了这个函数:

 getSimilarShows = (allShows, expectedShow) => {
      const titles = []
      Object.values(Object.values(allShows)).map((days) =>
        Object.values(days).map((items) =>
          Object.values(items).map((show) => {
              if (show.title === expectedShow) {
                titles.push(show.title)
              }
          })
        )
      )
    }

这给了我一个标题数组,但我还需要保存在该数组中的 ID、日期和节目。

这是数据示例:

{
1: {29: [{0: {id: 0000, title: 'some title'}, 
         {1: {id: 0000, title: 'some title'},
         ...], 
    30: [{0: {id: 0000, title: 'some title'}, 
         {1: {id: 0000, title: 'some title'},
         ...],
   ...}, 
6: {29: [{0: {id: 0000, title: 'some title'}, 
         {1: {id: 0000, title: 'some title'},
         ...], 
    30: [{0: {id: 0000, title: 'some title'}, 
         {1: {id: 0000, title: 'some title'},
         ...],
   ...},  
...}

如何正确保存?

【问题讨论】:

  • 你能发一个数据的例子吗?
  • @Greedo 这里是数据样本
  • 请包含代码库。
  • 执行 'someIds.push(show.id)' ,我没发现问题?
  • show.id 不是我需要的。我需要将对象键(1 或 6、29 或 30、0 或 1)推送到该数组,所以基本上如果标题等于某个值,它必须跟踪到该标题的整个路径。

标签: javascript arrays reactjs recursion javascript-objects


【解决方案1】:

您的数据结构并不是真正的递归。每个级别不仅代表不同类型的价值(某种组,一天,也许是一个事件),而且您的结构在不同级别上并不一致。 (为什么是层次结构中间的数组?)

所以递归处理不会在这里进行。但是我们可以用这样的方式以相当清晰的方式遍历结构:

const getSimilarShows = (shows, title) => 
  Object .entries (shows)
    .flatMap (([group, days]) =>
      Object .entries (days)
        .flatMap (([day, events]) => 
          events.flatMap ((ev) => 
            Object .entries (ev) 
              .filter (([_, {title: t}]) => t === title)
              .map (([event, {title, ...rest}]) => ({group, day, event, title, ...rest}))
          )
        )
    )


const shows = {
  1: {
    29: [
      {0: {id: '0001', title: 'title a'}},
      {1: {id: '0002', title: 'title b'}},
    ],
    30: [
      {0: {id: '0003', title: 'title c'}},
      {1: {id: '0004', title: 'title a'}},
    ]
  },
  6: {
    29: [
      {0: {id: '0005', title: 'title d'}},
      {1: {id: '0006', title: 'title b'}},
    ],
    30: [
      {0: {id: '0007', title: 'title a'}},
      {1: {id: '0008', title: 'title c'}},
    ]
  }
}

console .log (
  getSimilarShows (shows, 'title a')
)
.as-console-wrapper {max-height: 100% !important; top: 0}

我很少喜欢嵌套如此深的代码。但是我的第一种方法是从getSimilarShows 调用getDays 调用getEvents 开始的,并且在每个级别我都必须将结果映射回具有找到的级别键的对象(groupdayevent。 ) 它的代码比这个版本多得多,但仍然不清晰。

说到那些组键,我不得不编造它们。我不知道最外面的16(我称之为group)代表什么,也不知道重复的内部01(我称之为event)代表什么。我很确定2930 应该代表days。因此,您可能需要更改这些属性和相应的变量。

还有一个我没有命名的级别。我不是特别了解里面的结构,比如2930。为什么那里有一个单个整数键属性的数组,而不是像更高级别的对象?我没有在结果中包含这个索引。但如果你需要它,这一行:

          events.flatMap ((ev) => 

可以变成

          events.flatMap ((ev, index) => 

您可以将index 添加到返回的对象中。

不过,如果可以的话,我建议您研究一下该数组是否有必要。

【讨论】:

  • 我很高兴你决定解决这个问题。我想帮忙,但这个问题有很多问题,我认为我可能永远无法深入了解我想说的话。这说明了所有这些以及更多内容。
  • 我怀疑group 键实际上是month 数字......但我无法理解最里面的{ 0: ... }{ 1: ... } 对象。 OP 对数据结构的这些误用使得我们很难理解真正的意图,而且通常情况下,当您提出替代结构时,它会遭到回击。
  • 你的回答启发了我从未写过但非常有用的函数:D
  • @Thankyou:我很想听听更多!
  • 我将其发布为对您回答的赞美
【解决方案2】:

我们可以使用Object.entries()方法得到keys和它的values,然后根据你的情况直接filter它们:

const getArrayFromObject = (obj) => {
        let items = [];
        Object.entries(obj)
            .forEach(([k, v])=> Object.entries(v).forEach(([k1, v1]) =>
                v1.forEach(item => item.hasOwnProperty('0') ? items.push({ id: item[0].id, day: +k1, title: item[0].title, show: 0 }) :
                    items.push({ id: item[1].id, day: +k1, title: item[1].title, show: 1 }) )));
        return items;
    }

一个例子:

const obj = {
    1: {29: [
            { 0: {id: 0001, title: 'some title1'}},
            { 1: {id: 0002, title: 'some title2'}},
        ],
        30: [{0: {id: 0000, title: 'some title'}},
             {1: {id: 0000, title: 'some title'}},
             ],
       },
    6: {29: [{0: {id: 0000, title: 'some title'}},
             {1: {id: 0000, title: 'some title'}},
             ],
        30: [{0: {id: 0000, title: 'some title'}},
             {1: {id: 0000, title: 'some title'}},
             ],
       },
    };


const getArrayFromObject = (obj) => {
    let items = [];
    Object.entries(obj)
        .forEach(([k, v])=> Object.entries(v).forEach(([k1, v1]) =>
            v1.forEach(item => item.hasOwnProperty('0') ? items.push({ id: item[0].id, day: +k1, title: item[0].title, show: 0 }) :
                items.push({ id: item[1].id, day: +k1, title: item[1].title, show: 1 }) )));
    return items;
}

const result = getArrayFromObject(obj).filter(f => f.id == 1 && f.title == 'some title1');
console.log(result);

或者使用递归方法,可以从对象中获取所有数组,然后通过所需的键 filter 它:

const items = [];
const getArrayFromObject = obj => {
    for (var k in obj)
    {
        if (typeof obj[k] == "object" && obj[k] !== null)
            getArrayFromObject(obj[k]);
        else
            items.push(obj);
    }
}

getArrayFromObject(obj);
let result = items.filter(f => f.id == 1 && f.title == 'some title1');

一个例子:

const obj = {
    1: {29: [
            { 0: {id: 0001, title: 'some title1'}},
            { 1: {id: 0002, title: 'some title2'}},
        ],
        30: [{0: {id: 0000, title: 'some title'}},
             {1: {id: 0000, title: 'some title'}},
             ],
       },
    6: {29: [{0: {id: 0000, title: 'some title'}},
             {1: {id: 0000, title: 'some title'}},
             ],
        30: [{0: {id: 0000, title: 'some title'}},
             {1: {id: 0000, title: 'some title'}},
             ],
       },
    };

const items = [];
const getArrayFromObject = obj => {
    for (var k in obj)
    {
        if (typeof obj[k] == "object" && obj[k] !== null)
            getArrayFromObject(obj[k]);
        else
            items.push(obj);
    }
}

getArrayFromObject(obj);
let result = items.filter(f => f.id == 1 && f.title == 'some title1');

console.log(result)

如果我们想坚持上面的方法并且想要获取他们的密钥,那么我们可以使用以下方法:

const obj = {
1: {29: [
        { 0: {id: 0001, title: 'some title1'}},
        { 1: {id: 0002, title: 'some title2'}},
    ],
    30: [{0: {id: 0000, title: 'some title'}},
         {1: {id: 0000, title: 'some title'}},
         ],
   },
6: {29: [{0: {id: 0000, title: 'some title'}},
         {1: {id: 0000, title: 'some title'}},
         ],
    30: [{0: {id: 0000, title: 'some title'}},
         {1: {id: 0000, title: 'some title'}},
         ],
   },
};

let items = [];

const getArrayFromObject = (obj, keys) => {
    for (var k in obj)
    {
        if (typeof obj[k] == "object" && obj[k] !== null)
            getArrayFromObject(obj[k], keys ? `${keys}, ${k}` : k);
        else
            items.push({...obj, keys});
    }
}

getArrayFromObject(obj);
let uniqueItems = items.filter((f, index, self) =>
    index === self.findIndex((t) => (
        t.id === f.id && t.title === f.title
  )));

uniqueItems = uniqueItems.map(s => ({id: s.id, day: +(s.keys.split(',')[1]), show: +(s.keys.split(',')[2]), title: s.title }));
console.log(uniqueItems)

【讨论】:

  • 好的,这是递归函数的一个很好的例子,但是如何将对象键与标题一起保存?从上面的例子中我需要一个新对象 = {id: 1, day: 29, show: 0, title: "some title1"}?
【解决方案3】:

@Scott 帮您解释了您提出的数据形状和程序存在的问题。他是正确的,递归不是特别适合这个问题。他的回答确实激发了一个想法,我将在下面分享。

这里有collapse,它允许您使用named 键的可变长度序列折叠任意形状的对象 -

  1. 如果name 为空,则已达到基本情况。合并中间结果 r 和输入 t
  2. (感应)name 不为空。折叠输入 t 并以较小的子问题重复出现
const collapse = ([ name, ...more ], t = {}, r = {}) =>
  name === undefined
    ? [ { ...r, ...t } ]    // 1
    : Object                // 2
        .entries(t)
        .flatMap
          ( ([ k, v ]) =>
              collapse(more, v, { ...r, [name]: k }) // <- recursion
          )

const result =
  collapse(["month", "day", "event", "_"], shows)

console.log(JSON.stringify(result, null, 2))
[ { "month": "1", "day": "29", "event": "0", "_": "0", "id": "0001", "title": "title a" }
, { "month": "1", "day": "29", "event": "1", "_": "1", "id": "0002", "title": "title b" }
, { "month": "1", "day": "30", "event": "0", "_": "0", "id": "0003", "title": "title c" }
, { "month": "1", "day": "30", "event": "1", "_": "1", "id": "0004", "title": "title a" }
, { "month": "6", "day": "29", "event": "0", "_": "0", "id": "0005", "title": "title d" }
, { "month": "6", "day": "29", "event": "1", "_": "1", "id": "0006", "title": "title b" }
, { "month": "6", "day": "30", "event": "0", "_": "0", "id": "0007", "title": "title a" }
, { "month": "6", "day": "30", "event": "1", "_": "1", "id": "0008", "title": "title c" }
]

感谢collapse,现在写getSimilarShows 更容易了-

const getSimilarShows = (shows = [], query = "") =>
  collapse(["month", "day", "event", "_"], shows) // <-
    .filter(v => v.title === query)

const result =
  getSimilarShows(shows, "title b")

console.log(JSON.stringify(result, null, 2))
[ { "month": "1", "day": "29", "event": "1", "_": "1", "id": "0002", "title": "title b" }
, { "month": "6", "day": "29", "event": "1", "_": "1", "id": "0006", "title": "title b" }
]

注意

NB collapse 有点鲁莽,并不能保护您免于尝试过度折叠对象。例如,如果您提供四 (4) 个命名键,但对象仅嵌套两 (2) 层深,则将返回空结果 []。这可能是意料之外的,在这种情况下最好抛出运行时错误。

一个明显的改进是能够使用已知名称“跳过”关卡,例如上面的"_" -

const collapse = ([ name, ...more ], t = {}, r = {}) =>
  name === undefined
    ? [ { ...r, ...t } ]
    : Object
        .entries(t)
        .flatMap
          ( ([ k, v ]) =>
              name === "_"  // <- skip this level?
                ? collapse(more, v, r)  // <- new behaviour
                : collapse(more, v, { ...r, [name]: k }) // <- original
          )

const result =
  collapse(["month", "day", "event", "_"], shows)

console.log(JSON.stringify(result, null, 2))

有了这个更新,"_" 键不会出现在下面的输出中 -

[ { "month": "1", "day": "29", "event": "0", "id": "0001", "title": "title a" }
, { "month": "1", "day": "29", "event": "1", "id": "0002", "title": "title b" }
, { "month": "1", "day": "30", "event": "0", "id": "0003", "title": "title c" }
, { "month": "1", "day": "30", "event": "1", "id": "0004", "title": "title a" }
, { "month": "6", "day": "29", "event": "0", "id": "0005", "title": "title d" }
, { "month": "6", "day": "29", "event": "1", "id": "0006", "title": "title b" }
, { "month": "6", "day": "30", "event": "0", "id": "0007", "title": "title a" }
, { "month": "6", "day": "30", "event": "1", "id": "0008", "title": "title c" }
]

@Scott 提供了一个很好的建议,即使用原生 Symbol 或基于字符串的键。关注下方collapse.skip -

const collapse = (...) =>
  name === undefined
    ? //...
    : Object
        .entries(t)
        .flatMap
          ( ([ k, v ]) =>
              name === collapse.skip // <- known symbol
                ? //...
                : //...
          )

collapse.skip = // <- define symbol
  Symbol("skip") 

现在我们使用collapse.skip,而不是赋予"_" 特殊行为。为了使示例保持一致,我们只跳过了一层嵌套,但我们可以有效地跳过任意数量的嵌套 -

const result =
  collapse(["month", "day", "event", collapse.skip], shows) // <-

console.log(JSON.stringify(result, null, 2))
// ...

替代实施

我花了一些时间思考collapse,我想知道修改呼叫站点如何增加它的实用性 -

function collapse (t = {}, ...f)
{ function loop (t, c, r)
  { if (c >= f.length)
      return [ { ...r, ...t } ]
    else
      return Object
        .entries(t)
        .flatMap(([ k, v ]) => loop(v, c + 1, { ...r, ...f[c](k) }))
  }

  return loop(t, 0, {})
}

const shows =
  {1:{29:[{0:{id:'0001',title:'title a'}},{1:{id:'0002',title:'title b'}}],30:[{0:{id:'0003',title:'title c'}},{1:{id:'0004',title:'title a'}}]},6:{29:[{0:{id:'0005',title:'title d'}},{1:{id:'0006',title:'title b'}}],30:[{0:{id:'0007',title:'title a'}},{1:{id:'0008',title:'title c'}}]}}

const result =
  collapse
    ( shows
    , v => ({ month: v })
    , v => ({ day: v })
    , v => ({ event: v })
    , v => ({})             // <- "skip"
    )

console.log(JSON.stringify(result, null, 2))

类列表数组解构

虽然考虑数组索引很痛苦,但我同意下面@Scott 的评论。但是使用剩余参数进行解构可以创建很多中间值。这是一种技术,likeList,我一直在玩弄它似乎具有良好的人体工程学和内存占用 -

const likeList = (t = [], c = 0) =>
  ({ [Symbol.iterator]: function* () { yield t[c]; yield likeList(t, c + 1) } })

function collapse (t = {}, ...f)
{ function loop (t, [f, fs], r) // <- destructure without rest
  { if (f === undefined)        // <- base case: no f
      return [ { ...r, ...t } ]
    else
      return Object
        .entries(t)
        .flatMap(([ k, v ]) => loop(v, fs, { ...r, ...f(k) })) // <- f
  }

  return loop(t, likeList(f), {}) // <- likeList
}

或者可能 -

const likeList = (t = [], c = 0) =>
  ({ [Symbol.iterator]: _ => [ t[c], likeList(t, c + 1) ].values() })

保持性能

我是功能风格的大力倡导者,因为它释放了我们以完全不同的方式思考问题的能力。 JavaScript 对函数式程序员非常友好,但也有一些注意事项。以特定方式使用某些功能会减慢我们的程序速度,有时会让我们认为功能风格本身就是罪魁祸首。

我个人的爱好是探索新的方法来表达不会对性能造成很大影响的函数式程序。以上likeList 提供了解决方案。下面我们将在比较四 (4) 个复制数组的程序时对其进行测试。除了遍历输入数组的方式之外,每个程序都是相同的。

这里是用 rest 参数解构复制。 JavaScript 的原生解构语法支持的优雅形式。然而,它的成本很高,我们稍后会看到 -

const copyDestructure = (arr) =>
  loop
    ( ( [ x, ...xs ] = arr    // <- rest argument
      , r = []
      ) =>
        x === undefined
          ? r
          : recur(xs, push(r, x))
    )

这里是使用数字索引的副本。这将解构语法换成廉价索引。但是现在程序员有负担考虑数组边界、中间状态和非一错误-

const copyIndex = (arr) =>
  loop
    ( ( i = 0        // <- index
      , r = []
      ) =>
        i >= arr.length      // <- off-by-one?
          ? r
          : recur(i + 1, push(r, arr[i]))  // <- increment i
    )

这是使用likeList 的副本。这使用解构语法,但没有昂贵的 rest 参数。我们消除了使用索引的所有负面担忧,但我们能保持良好的性能吗? -

const copyLikeList = (arr) =>
  loop
    ( ( [ x, xs ] = likeList(arr) // <- likeList
      , r = []
      ) =>
        x === undefined
          ? r
          : recur(xs, push(r, x)) // <- plainly use x and xs
    )

并通过listList 复制,使用替代实现-

const copyLikeList2 = (arr) =>
  loop
    ( ( [ x, xs ] = likeList2(arr)  // <- implementation 2
      , r = []
      ) =>
        x === undefined
          ? r
          : recur(xs, push(r, x))   // <- same
    )

以毫秒为单位的运行时间,越低越好 -

Array size         100    1,000    10,000     100,000
-----------------------------------------------------
copyDestructure   3.30    19.23     482.3    97,233.5
copyIndex         0.47     5.92      20.9       165.1 <-
copyLikeList      1.18     9.31      55.6       479.2
copyLikeList2     0.79     7.90      33.6       172.4 <-

以 KB 为单位使用的内存,越低越好 -

Array size                1,000               100,000
-----------------------------------------------------
copyDestructure          613.43             38,790.34
copyIndex                247.60              4,133.72 <-
copyLikeList             960.44             26,885.91
copyLikeList2            233.63              2,941.98 <-

实施-

// Arr.js

const likeList = (t = [], c = 0) =>
  ({ [Symbol.iterator]: function* () { yield t[c]; yield likeList(t, c + 1) } })

const likeList2 = (t = [], c = 0) =>
  ({ [Symbol.iterator]: _ => [ t[c], likeList2(t, c + 1) ].values() })

const push = (t = [], x) =>
  ( t.push(x)
  , t
  )

const range = (start = 0, end = 0) =>
  Array.from(Array(end - start), (_, n) => n + start)

export { likeList, likeList2, push, range }
// TailRec.js

function loop (f, ...init)
{ let r = f(...init)
  while (r && r.recur === recur)
    r = f(...r)
  return r
}

const recur = (...v) =>
  ({ recur, [Symbol.iterator]: _ => v.values() })

export { loop, recur }

备注

copyLikeList2 上面使用likeList 的第二个实现真的很重要。性能特征与使用索引相当,即使在大输入上也是如此。 copyDestructure 即使在小至 1,000 个元素的数组上也明显变慢。

【讨论】:

  • 太棒了!一个可能的调整是将符号SKIP 作为collapse 函数的属性,以便在使用'_' 的地方明确使用。
  • 我特别喜欢对数组和其他对象的一致处理,这是我从您的其他答案中学到的,但有时我仍然忘记这样做。
  • @Scott,符号的完美用例。我喜欢它:D
  • 我想知道在替代实现中为什么选择使用数组索引而不是在 [f, ...fs] 参数上重复出现。我的版本可能是look like this
  • @ScottSauyet 我还没有完成它,最近我已经因为其他事情被拉到一边了:(它背后的基本思想是t = spread(spread(1), 2, spread(), [3,4], spread(5, [6]))toArray(t) 产生[1,2,3,4,5,6]。优化是 spread 本质上是构造一个 AST,而不是复制整个数组。当调用 toArray 时,会创建一个数组并将扁平化的 AST 复制到其中一次。可以这样做对于toObject
【解决方案4】:

在提高可维护性和可读性时使用库的忠实拥护者。这是使用object-scan 的解决方案。我们将它用于大多数与数据处理相关的任务。一旦你了解如何使用它,它就会非常强大。

// const objectScan = require('object-scan');

const extract = (title, data) => objectScan(['*.*[*].*'], {
  filterFn: ({ key, value, context }) => {
    if (value.title === title) {
      const [group, day, _, event] = key;
      context.push({ group, day, event, ...value });
    }
  }
})(data, []);

const shows = { 1: { 29: [{ 0: { id: '0001', title: 'title a' } }, { 1: { id: '0002', title: 'title b' } }], 30: [{ 0: { id: '0003', title: 'title c' } }, { 1: { id: '0004', title: 'title a' } }] }, 6: { 29: [{ 0: { id: '0005', title: 'title d' } }, { 1: { id: '0006', title: 'title b' } }], 30: [{ 0: { id: '0007', title: 'title a' } }, { 1: { id: '0008', title: 'title c' } }] } };

console.log(extract('title a', shows));
/* =>
  [ { group: '6', day: '30', event: '0', id: '0007', title: 'title a' },
    { group: '1', day: '30', event: '1', id: '0004', title: 'title a' },
    { group: '1', day: '29', event: '0', id: '0001', title: 'title a' } ]
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
&lt;script src="https://bundle.run/object-scan@13.8.0"&gt;&lt;/script&gt;

免责声明:我是object-scan的作者

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-04-13
    • 2022-11-17
    • 2022-11-03
    • 1970-01-01
    • 2015-07-10
    • 1970-01-01
    • 2017-09-19
    相关资源
    最近更新 更多