如何使用递归 JavaScript 映射方法从父/子关系创建新对象 [英] How to create a new object from parent/child relationships using recursive JavaScript map method

查看:29
本文介绍了如何使用递归 JavaScript 映射方法从父/子关系创建新对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个对象数组.其中一些有一个值为 ` 的 wordpress_parent 道具.这意味着这个节点是另一个节点的子节点.实际的最终结果是一个嵌套的评论 UI,因此可以有多个级别的子级.

I've got an array of objects. Some of them have a wordpress_parent prop with a value `. This means this node is a child of another node. The actual end result is a nested comment UI, so there can be multiple levels of children.

我想遍历我的对象,在 wordpress_parent !== 0 处,在原始数组中找到 wordpress_id 等于 值的对象wordpress_parent 并将该对象作为匹配父节点的子属性.

I'd like to loop through my objects and where wordpress_parent !== 0, find the object in the original array whose wordpress_id equals the value of wordpress_parent and put that object as a child property of the matching parent node.

我要实现这个对象形式:

I want to achieve this object form:

node {
    ...originalPropsHere,
    children: { ...originalChildNodeProps } 
}

我们的想法是创建一个新数组,该数组具有正确的父级和子级嵌套结构,然后我可以对其进行迭代并输出到 JSX 结构中.

The idea is to create a new array with the proper nested structure of parent, children that I can then iterate over and pump out into a JSX structure.

我想写一个递归函数来执行这个逻辑,然后返回一个像这样的 JSX 注释结构(基本上):

I want to write a recursive function that does this logic and then returns a JSX comment structure like this (basically):

<article className="comment" key={node.wordpress_id}>
    <header>
        <a href={node.author_url} rel="nofollow"><h4>{node.author_name}</h4></a>
        <span>{node.date}</span>
    </header>
    {node.content}
</article>

我想我必须使用 JavaScript 的 map 方法来创建一个新数组.我遇到的问题是操作数据以在我的父节点上创建一个新的 children 属性,然后将匹配的子注释作为该属性的值.然后将它放在一个漂亮的小函数中,该函数递归执行并创建我可以在我的组件中呈现的 HTML/JSX 结构.

I figure I have to use JavaScripts' map method to create a new array. The trouble I'm having is manipulating the data to create a new children property on my parent nodes, then placing the matching child comment as the value of that property. Then placing that in a nice little function that recursively goes through and creates the HTML/JSX structure that I can render in my components.

聪明的人,请站出来,谢谢!:D

Smarter folks, step right up please, and thank you! :D

推荐答案

这是对another answer的修改,它处理额外的 node 包装器和您的 id 和父属性名称:

This is a modification of another answer, which handles the extra node wrapper and your id and parent property names:

const nest = (xs, id = 0) => 
  xs .filter (({node: {wordpress_parent}}) => wordpress_parent == id)
     .map (({node: {wordpress_id, wordpress_parent, ...rest}}) => ({
       node: {
         ...rest,
         wordpress_id, 
         children: nest (xs, wordpress_id)
       }
     }))

const edges = [
  {node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}},
  {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}},
  {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}},
  {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}},
  {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}},
  {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}},
  {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}
]

console .log (
  nest (edges)
)

.as-console-wrapper {max-height: 100% !important; top: 0}

它在那些没有子节点的节点中包含一个空的 children 数组.(如果你知道你最多只能有一个孩子并且更喜欢 child 这个名字,那么这需要一些修改;但它应该不错.)

It includes an empty children array in those nodes without children. (If you know that you can only have at most one child and prefer the name child, then this would take a little reworking; but it shouldn't be bad.)

基本上,它需要一个项目列表和一个 id 来测试,并过滤具有该 id 的列表.然后它通过使用当前对象的 id 递归调用函数来添加一个 children 属性.

Basically, it takes a list of items and an id to test, and filters the list for those that have that id. Then it adds a children property by recursively calling the function with the id of the current object.

因为 wordpress_parent 包含在传递给 map 的函数的解构参数中,但未包含在输出中,因此跳过此节点.如果你想保留它,你可以将它添加到输出中,但更容易的是将它作为参数跳过;那么它将成为 ...rest 的一部分.

Because wordpress_parent is included in the destructured parameters for the function passed to map but not included in the output, this node is skipped. If you want to keep it, you could add it to the output, but even easier is skipping it as a parameter; then it will be part of ...rest.

Thankyou 的回答很鼓舞人心.我已经用相同答案的变体回答了很多这样的问题.现在是泛化到可重用功能的时候了.

The answer from Thankyou is inspirational. I've answered quite a few questions like this with variants of the same answer. It's past time to generalize to a reusable function.

该答案创建所有值的索引,然后使用该索引构建输出.我上面的技术(以及其他几个答案)有些不同:扫描所有根元素的数组,对于每个根元素,扫描其子元素的数组,以及为每个子元素扫描数组,等等.这可能效率较低,但更容易推广,因为无需为每个元素生成代表键.

That answer creates an index of all values and then builds the output using the index. My technique above (and in several other answers) is somewhat different: scanning the array for all root elements and, for each one, scanning the array for their children, and, for each of those, scanning the array for the grandchildren, etc. This is probably less efficient, but is slightly more easily generalized, as there is no need to generate a representative key for each element.

所以我在一个更通用的解决方案中创建了第一遍,其中我上面所做的被分离成两个更简单的函数,这些函数被传递(连同原始数据和根值的表示)到一个泛型将它们放在一起并处理递归的函数.

So I've created a first pass at a more general solution, in which what I do above is separated out into two simpler functions which are passed (along with the original data and a representation of the root value) into a generic function which puts those together and handles the recursion.

以下是使用此函数解决当前问题的示例:

Here's an example of using this function for the current problem:

// forest :: [a] -> (a, (c -> [b]) -> b) -> ((c, a) -> Bool) -> c -> [b]
const forest = (xs, build, isChild, root) => 
  xs .filter (x => isChild (root, x))
     .map (node => build (node, root => forest (xs, build, isChild, root)))
    
const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]

const result = forest (
  edges,     
  (x, f) => ({node: {...x.node, children: f (x.node.wordpress_id)}}),
  (id, {node: {wordpress_parent}}) => wordpress_parent == id,
  0
)

console .log (result)

.as-console-wrapper {min-height: 100% !important; top: 0}

我使用 forest 而不是 tree,因为这里生成的实际上不是树.(它有多个根节点.)但它的参数与Thankyou的参数非常相似.其中最复杂的 build 完全等同于该答案的 maker.xs 等效于 all,并且 root 参数(几乎)等效.主要区别在于Thankyou 的indexer 和我的isChild 之间.由于Thankyou 生成元素的外键映射,indexer 接受一个节点并返回该节点的表示,通常是一个属性.我的版本是一个二进制谓词.它接受当前元素和第二个元素的表示,并当且仅当第二个元素是当前元素的子元素时返回 true.

I use forest rather than tree, as what's generated here is not actually a tree. (It has multiple root nodes.) But its parameters are very similar to those from Thankyou. The most complex of them, build is exactly equivalent to that answer's maker. xs is equivalent to all, and the root parameters are (nearly) equivalent. The chief difference is between Thankyou's indexer and my isChild. Because Thankyou generates a Map of foreign keys to elements, indexer takes a node and returns a representation of the node, usually a property. My version instead is a binary predicate. It takes a representation of the current element and a second element and returns true if and only if the second element is a child of the current one.

最后一个参数,root,实际上相当有趣.它需要是当前对象的某种代表.但它不需要是任何特定的代表.在简单的情况下,这可以是类似于 id 参数的东西.但它也可以是实际元素.这也可以:

The final parameter, root, is actually fairly interesting. It needs to be some sort of representative of the current object. But it does not need to be any particular representative. In simple cases, this can just be something like an id parameter. But it can also be the actual element. This would also work:

console .log (forest (
  edges,
  (x, f) => ({node: {...x.node, children: f (x)}}),
  (p, c) => p.node.wordpress_id == c.node.wordpress_parent,
  {node: {wordpress_id: 0}}
))

.as-console-wrapper {max-height: 100% !important; top: 0}

<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))
        const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]</script>

在这种情况下,最后一个参数更复杂,它是一个结构类似于列表中典型元素的对象,在这种情况下具有根 ID.但是当我们这样做时,参数 isChildbuild 提供的回调会更简单一些.要记住的是,这是传递给 isChild 的结构.在第一个示例中,它只是 id,所以 root 参数很简单,但其他函数有点复杂.在第二个中,root 更复杂,但它允许我们简化其他参数.

In this case, the final parameter is more complex, being an object structurally similar to a typical element in the list, in this case with the root id. But when we do this, the parameters isChild and to the callback supplied by build are a bit simpler. The thing to keep in mind is that this is the structure passed to isChild. In the first example that was just the id, so the root parameter was simple, but those other functions were a bit more complex. In the second one, root was more complex, but it allowed us to simplify the other parameters.

这可以很容易地应用于其他示例.前面提到的前面的问题可以这样处理:

This can easily be applied to other examples. The earlier question mentioned before can be handled like this:

const flat = [
  {id: "a", name: "Root 1", parentId: null}, 
  {id: "b", name: "Root 2", parentId: null}, 
  {id: "c", name: "Root 3", parentId: null}, 
  {id: "a1", name: "Item 1", parentId: "a"}, 
  {id: "a2", name: "Item 1", parentId: "a"}, 
  {id: "b1", name: "Item 1", parentId: "b"}, 
  {id: "b2", name: "Item 2", parentId: "b"}, 
  {id: "b2-1", name: "Item 2-1", parentId: "b2"}, 
  {id: "b2-2", name: "Item 2-2", parentId: "b2"}, 
  {id: "b3", name: "Item 3", parentId: "b"}, 
  {id: "c1", name: "Item 1", parentId: "c"}, 
  {id: "c2", name: "Item 2", parentId: "c"}
]

console .log (forest (
  flat,
  ({id, parentId, ...rest}, f) => ({id, ...rest, children: f (id)}),
  (id, {parentId}) => parentId == id,
  null
))

.as-console-wrapper {max-height: 100% !important; top: 0}

<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>

或者感谢提供的示例可能如下所示:

const input = [
  { forumId: 3, parentId: 1, forumName: "General", forumDescription: "General forum, talk whatever you want here", forumLocked: false, forumDisplay: true }, 
  { forumId: 2, parentId: 1, forumName: "Announcements", forumDescription: "Announcements & Projects posted here", forumLocked: false, forumDisplay: true }, 
  { forumId: 4, parentId: 3, forumName: "Introduction", forumDescription: "A warming introduction for newcomers here", forumLocked: false, forumDisplay: true }, 
  { forumId: 1, parentId: null, forumName: "Main", forumDescription: "", forumLocked: false, forumDisplay: true }
]

console .log (forest (
  input,
  (node, f) => ({...node, subforum: f(node .forumId)}),
  (id, {parentId}) => parentId == id,
  null
))

.as-console-wrapper {max-height: 100% !important; top: 0}

<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>

这些输入结构的相似之处在于每个节点都指向其父节点的标识符,当然根节点除外.但是这种技术同样适用于父母指向他们孩子的标识符列表的技术.创建根元素(这里还有一个辅助函数)需要更多的工作,但相同的系统将允许我们对这样的模型进行水合:

These input structures all are similar in that each node points to an identifier for its parent, except of course the root nodes. But this technique would work just as well with one where parents point to a list of identifiers for their children. It takes a bit more work to create the root element (and here a helper function as well) but the same system will allow us to hydrate such a model:

const xs = [
  {content: 'abc', wordpress_id: 196, child_ids: []},
  {content: 'def', wordpress_id: 193, child_ids: [196, 199]},
  {content: 'ghi', wordpress_id: 199, child_ids: []},
  {content: 'jkl', wordpress_id: 207, child_ids: [208, 224]},
  {content: 'mno', wordpress_id: 208, child_ids: [209]},
  {content: 'pqr', wordpress_id: 209, child_ids: []},
  {content: 'stu', wordpress_id: 224, child_ids: []}
]

const diff = (xs, ys) => xs .filter (x => !ys.includes(x))

console .log (forest (
  xs,
  (node, fn) => ({...node, children: fn(node)}),
  ({child_ids}, {wordpress_id}) => child_ids .includes (wordpress_id),
  {child_ids: diff (xs .map (x => x .wordpress_id), xs .flatMap (x => x .child_ids))}
))

.as-console-wrapper {max-height: 100% !important; top: 0}

<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>

这里我们有一个不同风格的isChild,测试潜在孩子的id是否在parent提供的id列表中.并且为了创建初始根,我们必须扫描那些没有作为子 ID 出现的 ID 列表.我们使用 diff 助手来做到这一点.

Here we have a different style of isChild, testing whether the potential child's id is in the list of ids supplied by the parent. And to create the initial root we have to scan the list of ids for those that do not appear as child ids. We use a diff helper to do this.

我在上面讨论额外的灵活性时提到了这种不同的风格.

This different style is what I referred to above when discussing additional flexibility.

我称之为第一次通过";在这样的解决方案中,因为这里有一些我不太满意的地方.我们可以使用这种技术来处理删除现在不需要的父 id,并且如果实际上有实际的孩子要包含,也可以只包含一个 children 节点.对于原始示例,它可能如下所示:

I called this a "first pass" at such a solution because there's something I'm not really happy with here. We can use this technique to deal with removing now-unnecessary parent ids, and also to only include a children node if there are, in fact, actual children to include. For the orginal example, it might look like this:

console .log (forest (
  edges,
  ( {node: {wordpress_id, wordpress_parent, ...rest}}, 
    f, 
    kids = f (wordpress_id)
  ) => ({node: {
    ...rest,
    wordpress_id,
    ...(kids.length ? {children: kids} : {})
  }}),
  (id, {node: {wordpress_parent}}) => wordpress_parent == id,
  0
))

.as-console-wrapper {max-height: 100% !important; top: 0}

<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))
        const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]</script>

请注意,结果现在只包括 children,如果有的话.并且删除了现在多余的 wordpress_parent 节点.

Note that the results now only include children if there's something there. And the wordpress_parent node, now redundant, has been removed.

所以这可以通过这种技术实现,我们可以对其他示例做类似的事情.但是它在 build 函数中具有相当高的复杂性.我希望进一步的反思可以产生一种简化这两个特征的方法.所以它仍在进行中.

So this is possible to achieve with this technique, and we could do similar things for the other examples. But it comes at a fairly high complexity in the build function. I'm hoping that further reflection can yield a way to simplify those two features. So it's still a work in progress.

这种概括,将此类可重用的功能/模块保存为个人工具包的一部分,可以极大地改进我们的代码库.我们刚刚对许多明显相关但略有不同的行为使用了上述相同的函数.那只能是一场胜利.

This sort of generalization, saving such reusable functions/modules as part of a personal toolkit, can vastly improve our codebases. We have just used the same function above for a number of obviously related, but subtly different behaviors. That can be nothing but a win.

这不是完整的代码,但它可以像这样使用,并且有几个改进的途径可以追求.

This is not completed code, but it's usable like this, and there are several avenues of improvement to pursue.

非常感谢您的灵感.我可能早就应该这样做了,但这次不知何故它传到了我身上.谢谢!

A huge shout-out to Thankyou for the inspiration. I probably should have done this a while ago, but somehow this time it got through to me. Thanks!

这篇关于如何使用递归 JavaScript 映射方法从父/子关系创建新对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆