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

查看:61
本文介绍了如何使用递归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

推荐答案

这是对另一个答案的修改,用于处理额外的 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.但是,当我们执行此操作时,参数 isChild build 提供的回调要简单一些.要记住的是,这是传递给 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>

或者提供的示例Thankyou 可能看起来像这样:

Or the example Thankyou supplied might look like this:

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是否在父母提供的ID列表中.为了创建初始根目录,我们必须扫描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天全站免登陆