【问题标题】:Recursively filter array of objects递归过滤对象数组
【发布时间】:2016-11-03 01:59:12
【问题描述】:

用这个撞墙,我想我会把它贴在这里,以防万一有好心人遇到类似的。我有一些看起来像这样的数据:

const input = [
  {
    value: 'Miss1',
    children: [
      { value: 'Miss2' },
      { value: 'Hit1', children: [ { value: 'Miss3' } ] }
    ]
  },
  {
    value: 'Miss4',
    children: [
      { value: 'Miss5' },
      { value: 'Miss6', children: [ { value: 'Hit2' } ] }
    ]
  },
  {
    value: 'Miss7',
    children: [
      { value: 'Miss8' },
      { value: 'Miss9', children: [ { value: 'Miss10' } ] }
    ]
  },
  {
    value: 'Hit3',
    children: [
      { value: 'Miss11' },
      { value: 'Miss12', children: [ { value: 'Miss13' } ] }
    ]
  },
  {
    value: 'Miss14',
    children: [
      { value: 'Hit4' },
      { value: 'Miss15', children: [ { value: 'Miss16' } ] }
    ]
  },
];

在运行时我不知道层次结构有多深,即有多少级别的对象会有一个子数组。我已经稍微简化了这个例子,我实际上需要将值属性与一组搜索词进行匹配。让我们暂时假设我匹配value.includes('Hit') 的位置。

我需要一个返回新数组的函数,例如:

  • 每个没有子对象的不匹配对象,或子层次结构中没有匹配的对象,都不应存在于输出对象中

  • 每个具有包含匹配对象的后代的对象都应该保留

  • 匹配对象的所有后代应保留

我正在考虑一个“匹配对象”是一个具有value 属性的对象,在这种情况下包含字符串Hit,反之亦然。

输出应如下所示:

const expected = [
  {
    value: 'Miss1',
    children: [
      { value: 'Hit1', children: [ { value: 'Miss3' } ] }
    ]
  },
  {
    value: 'Miss4',
    children: [
      { value: 'Miss6', children: [ { value: 'Hit2' } ] }
    ]
  },
  {
    value: 'Hit3',
    children: [
      { value: 'Miss11' },
      { value: 'Miss12', children: [ { value: 'Miss13' } ] }
    ]
  },
  {
    value: 'Miss14',
    children: [
      { value: 'Hit4' },
    ]
  }
];

非常感谢花时间阅读本文的任何人,如果我先到那里,将发布我的解决方案。

【问题讨论】:

  • 所以您似乎是在说最终,只有最外层数组中的对象才会包含在结果中,因为如果嵌套在其中的任何内容匹配,则将保留其整个结构。在这种情况下,您似乎只需要input.filter(function(o) {...}),该函数进行递归调用,当/如果最终找到匹配项时返回true
  • @squint 不完全是。注意Miss2 是如何从Miss1 的子级中删除的
  • @4castle:好点,但我想知道为什么其他人没有被删除。我错过了什么?
  • @squint 仔细阅读他们的标准。输出和他们说的完全一样。
  • 啊,一击的所有后代……

标签: javascript arrays recursion filter


【解决方案1】:

使用.filter() 并按照我在上面的评论中描述的进行递归调用基本上是您所需要的。您只需要在返回之前使用递归调用的结果更新每个 .children 属性。

返回值只是生成的.children 集合的.length,因此如果至少有一个,则保留该对象。

var res = input.filter(function f(o) {
  if (o.value.includes("Hit")) return true

  if (o.children) {
    return (o.children = o.children.filter(f)).length
  }
})

const input = [
  {
    value: 'Miss1',
    children: [
      { value: 'Miss2' },
      { value: 'Hit1', children: [ { value: 'Miss3' } ] }
    ]
  },
  {
    value: 'Miss4',
    children: [
      { value: 'Miss5' },
      { value: 'Miss6', children: [ { value: 'Hit2' } ] }
    ]
  },
  {
    value: 'Miss7',
    children: [
      { value: 'Miss8' },
      { value: 'Miss9', children: [ { value: 'Miss10' } ] }
    ]
  },
  {
    value: 'Hit3',
    children: [
      { value: 'Miss11' },
      { value: 'Miss12', children: [ { value: 'Miss13' } ] }
    ]
  },
  {
    value: 'Miss14',
    children: [
      { value: 'Hit4' },
      { value: 'Miss15', children: [ { value: 'Miss16' } ] }
    ]
  },
];

var res = input.filter(function f(o) {
  if (o.value.includes("Hit")) return true

  if (o.children) {
    return (o.children = o.children.filter(f)).length
  }
})
console.log(JSON.stringify(res, null, 2))

请注意,String 上的 .includes() 是 ES7,因此可能需要针对旧版浏览器进行修补。您可以使用传统的.indexOf("Hit") != -1 代替它。


为了不改变原始数据,创建一个复制对象的映射函数并在过滤器之前使用它。

function copy(o) {
  return Object.assign({}, o)
}

var res = input.map(copy).filter(function f(o) {
  if (o.value.includes("Hit")) return true

  if (o.children) {
    return (o.children = o.children.map(copy).filter(f)).length
  }
})

要真正压缩代码,您可以这样做:

var res = input.filter(function f(o) {
  return o.value.includes("Hit") ||
         o.children && (o.children = o.children.filter(f)).length
})

虽然读起来有点难。

【讨论】:

  • 它改变了原始input 的内容。您可能首先要使用JSON.parse(JSON.stringify(input)) 制作副本。另外,我感觉这是服务器端的 JS。
  • 最简单的方法是从一开始就克隆整个结构。如果每个对象确实只有 2 个属性,那么在 .filter() 之前先对每个对象执行 .map() 应该很简单。
  • @squint 如果children 数组不包含"Hit" 其中value 前面的对象value 包含"Hit",是否要求在结果中包含children 数组?例如,过滤数组的索引 3 处的项目?
  • @guest271314:我想你可能会像我原来的想法一样,即使某物有一个“命中”的后代,所有它的后代也会是保留。这实际上也很容易,但从描述和显示的输出来看,唯一保留的未命中是“命中”的后代或某个“命中”的祖先。
  • 您能解释一下为什么return (o.children = o.children.filter(f)).length 中需要length 吗?
【解决方案2】:

这是一个功能,可以满足您的需求。本质上,它将测试arr 中的每个项目是否匹配,然后在其children 上递归调用过滤器。还使用了Object.assign,以便不会更改底层对象。

function filter(arr, term) {
    var matches = [];
    if (!Array.isArray(arr)) return matches;

    arr.forEach(function(i) {
        if (i.value.includes(term)) {
            matches.push(i);
        } else {
            let childResults = filter(i.children, term);
            if (childResults.length)
                matches.push(Object.assign({}, i, { children: childResults }));
        }
    })

    return matches;
}

【讨论】:

  • 谢谢,这种方法实际上让我更清楚我哪里出错了
  • 我认为将其称为“过滤器”会让人困惑,因为“过滤器”已经是 Javascript 中的数组方法。我们可以称之为“搜索”
【解决方案3】:

我认为这将是一个递归解决方案。这是我尝试过的一个。

function find(obj, key) {
  if (obj.value && obj.value.indexOf(key) > -1){
    return true;
  }
  if (obj.children && obj.children.length > 0){
    return obj.children.reduce(function(obj1, obj2){
      return find(obj1, key) || find(obj2, key);
    }, {}); 
  } 
  return false;
}

var output = input.filter(function(obj){
     return find(obj, 'Hit');
 });
console.log('Result', output);

【讨论】:

    【解决方案4】:

    const input = [
      {
        value: 'Miss1',
        children: [
          { value: 'Miss1' },
          { value: 'Hit1', children: [ { value: 'Miss3' } ] }
        ]
      },
      {
        value: 'Miss4',
        children: [
          { value: 'Miss5' },
          { value: 'Miss6', children: [ { value: 'Hit2' } ] }
        ]
      },
      {
        value: 'Miss7',
        children: [
          { value: 'Miss8' },
          { value: 'Miss9', children: [ { value: 'Miss10' } ] }
        ]
      },
      {
        value: 'Hit3',
        children: [
          { value: 'Miss11' },
          { value: 'Miss12', children: [ { value: 'Miss13' } ] }
        ]
      },
      {
        value: 'Miss14asds',
        children: [
          { value: 'Hit4sdas' },
          { value: 'Miss15', children: [ { value: 'Miss16' } ] }
        ]
      },
    ];
    
    function filter(arr, term) {
        var matches = [];
        
        if (!Array.isArray(arr)) return matches;
    
        arr.forEach(function(i) {
         
            if (i.value === term) {
        
             const filterData =  (i.children && Array.isArray(i.children))? i.children.filter(values => values.value ===term):[];
             console.log(filterData)
             i.children =filterData;
                matches.push(i);
              
            } else {
           // console.log(i.children)
                let childResults = filter(i.children, term);
                if (childResults.length)
         matches.push(Object.assign({}, i, { children: childResults }));
            }
        })
    
        return matches;
    }
    
    
    const filterData= filter(input,'Miss1');
    console.log(filterData)

    以下代码使用递归函数过滤父子数组数据

    const input = [
      {
        value: 'Miss1',
        children: [
          { value: 'Miss2' },
          { value: 'Hit1', children: [ { value: 'Miss3' } ] }
        ]
      },
      {
        value: 'Miss4',
        children: [
          { value: 'Miss5' },
          { value: 'Miss6', children: [ { value: 'Hit2' } ] }
        ]
      },
      {
        value: 'Miss7',
        children: [
          { value: 'Miss8' },
          { value: 'Miss9', children: [ { value: 'Miss10' } ] }
        ]
      },
      {
        value: 'Hit3',
        children: [
          { value: 'Miss11' },
          { value: 'Miss12', children: [ { value: 'Miss13' } ] }
        ]
      },
      {
        value: 'Miss14',
        children: [
          { value: 'Hit4' },
          { value: 'Miss15', children: [ { value: 'Miss16' } ] }
        ]
      },
    ];
    
    var res = input.filter(function f(o) {
      if (o.value.includes("Hit")) return true
    
      if (o.children) {
        return (o.children = o.children.filter(f)).length
      }
    })
    console.log(JSON.stringify(res, null, 2))

    【讨论】:

      【解决方案5】:

      这是一个很好的解决方案,它利用了数组reduce 函数,它产生的代码比其他解决方案更具可读性。在我看来,它也更具可读性。我们递归调用 filter 函数来过滤数组及其子数组

      const input = [
        {
          value: "Miss1",
          children: [
            { value: "Miss2" },
            { value: "Hit1", children: [{ value: "Miss3" }] },
          ],
        },
        {
          value: "Miss4",
          children: [
            { value: "Miss5" },
            { value: "Miss6", children: [{ value: "Hit2" }] },
          ],
        },
        {
          value: "Miss7",
          children: [
            { value: "Miss8" },
            { value: "Miss9", children: [{ value: "Miss10" }] },
          ],
        },
        {
          value: "Hit3",
          children: [
            { value: "Miss11" },
            { value: "Miss12", children: [{ value: "Miss13" }] },
          ],
        },
        {
          value: "Miss14",
          children: [
            { value: "Hit4" },
            { value: "Miss15", children: [{ value: "Miss16" }] },
          ],
        },
      ];
      
      function recursiveFilter(arr) {
        return arr.reduce(function filter(prev, item) {
          if (item.value.includes("Hit")) {
            if (item.children && item.children.length > 0) {
              return prev.concat({
                ...item,
                children: item.children.reduce(filter, []),
              });
            } else {
              return item;
            }
          }
          return prev;
        }, []);
      }
      
      console.log(recursiveFilter(input));

      【讨论】:

      • 请提交其他问题的现有解决方案,前提是它们确实解决了手头的问题;该问题与routes 或isAuthorized. 无关。如果有类比,请进行类比。此外,您的简要介绍不足以阻止它成为纯代码解决方案;不要发布那些。最后,在复活一个老问题时,请确保你有新的东西要添加。
      • @ScottSauyet 抱歉缺少上下文,我添加了必要的更改,我觉得与其他解决方案相比,这个特定的解决方案更具可读性和合理性。
      • @mailk:还是不行。第 3 行缺少 ),但即使已修复,您仍然有一个未定义的 route 变量。请在编写 JS/HTML/CSS 解决方案时,如果可能的话,创建一个snippet 来展示它的工作原理。
      • @ScottSauyet 抱歉……添加了必要的更改。它似乎工作正常
      • @mailk:问题中有请求的输出。这不匹配它,甚至不接近。接受的答案和这里的其他几个答案。我认为如果你迟到了,你真的应该得到一个符合要求的解决方案。
      【解决方案6】:

      Array.prototype.flatMap 非常适合递归过滤。类似于mapfilterreduce,使用flatMap 不会修改原始输入 -

      const findHits = (t = []) =>
        t.flatMap(({ value, children }) => {
          if (value.startsWith("Hit"))
            return [{ value, children }]
          else {
            const r = findHits(children)
            return r.length ? [{ value, children: r }] : []
          }
        })
      
      const input =
        [{value:'Miss1',children:[{value:'Miss2'},{value:'Hit1', children:[{value:'Miss3'}]}]},{value:'Miss4',children:[{value:'Miss5'},{value:'Miss6', children:[{value:'Hit2'}]}]},{value:'Miss7',children:[{value:'Miss8'},{value:'Miss9', children:[{value:'Miss10'}]}]},{value:'Hit3',children:[{value:'Miss11'},{value:'Miss12', children:[{value:'Miss13'}]}]},{value:'Miss14',children:[{value:'Hit4'},{value:'Miss15', children:[{value:'Miss16'}]}]}]
      
      const result =
        findHits(input)
      
      console.log(JSON.stringify(result, null, 2))
      [
        {
          "value": "Miss1",
          "children": [
            {
              "value": "Hit1",
              "children": [
                {
                  "value": "Miss3"
                }
              ]
            }
          ]
        },
        {
          "value": "Miss4",
          "children": [
            {
              "value": "Miss6",
              "children": [
                {
                  "value": "Hit2"
                }
              ]
            }
          ]
        },
        {
          "value": "Hit3",
          "children": [
            {
              "value": "Miss11"
            },
            {
              "value": "Miss12",
              "children": [
                {
                  "value": "Miss13"
                }
              ]
            }
          ]
        },
        {
          "value": "Miss14",
          "children": [
            {
              "value": "Hit4"
            }
          ]
        }
      ]
      

      【讨论】:

        【解决方案7】:

        或者,您可以使用deepdash 扩展中的_.filterDeep 方法lodash

        var keyword = 'Hit';
        var foundHit = _.filterDeep(
          input,
          function(value) {
            return value.value.includes(keyword);
          },
          {
            tree: true,
            onTrue: { skipChildren: true },
          }
        );
        

        这里是 full test 用于您的情况

        【讨论】:

          【解决方案8】:

          这是使用object-scan 的另一种解决方案。

          这个解决方案是迭代的而不是递归的。它之所以有效,是因为对象扫描以删除安全顺序进行迭代。基本上我们遍历树并在任何匹配时中断。然后我们跟踪不同深度的匹配(确保我们在遍历相邻分支时适当地重置该信息)。

          这主要是一种学术练习,因为递归方法更干净、更快捷。但是,如果需要完成额外的处理或堆栈深度错误是一个问题,这个答案可能会很有趣。

          // const objectScan = require('object-scan');
          
          const myInput = [{ value: 'Miss1', children: [{ value: 'Miss2' }, { value: 'Hit1', children: [{ value: 'Miss3' }] }] }, { value: 'Miss4', children: [{ value: 'Miss5' }, { value: 'Miss6', children: [{ value: 'Hit2' }] }] }, { value: 'Miss7', children: [{ value: 'Miss8' }, { value: 'Miss9', children: [{ value: 'Miss10' }] }] }, { value: 'Hit3', children: [{ value: 'Miss11' }, { value: 'Miss12', children: [{ value: 'Miss13' }] }] }, { value: 'Miss14', children: [{ value: 'Hit4' }, { value: 'Miss15', children: [{ value: 'Miss16' }] }] }];
          const myFilterFn = ({ value }) => value.includes('Hit');
          
          const rewrite = (input, filterFn) => objectScan(['**(^children$)'], {
            breakFn: ({ isMatch, value }) => isMatch && filterFn(value),
            filterFn: ({
              parent, property, key, value, context
            }) => {
              const len = (key.length - 1) / 2;
              if (context.prevLen <= len) {
                context.matches.length = context.prevLen + 1;
              }
              context.prevLen = len;
          
              if (context.matches[len + 1] === true || filterFn(value)) {
                context.matches[len] = true;
                return false;
              }
              parent.splice(property, 1);
              return true;
            },
            useArraySelector: false,
            rtn: 'count'
          })(input, { prevLen: 0, matches: [] });
          
          console.log(rewrite(myInput, myFilterFn)); // returns number of deletions
          // => 8
          
          console.log(myInput);
          // => [ { value: 'Miss1', children: [ { value: 'Hit1', children: [ { value: 'Miss3' } ] } ] }, { value: 'Miss4', children: [ { value: 'Miss6', children: [ { value: 'Hit2' } ] } ] }, { value: 'Hit3', children: [ { value: 'Miss11' }, { value: 'Miss12', children: [ { value: 'Miss13' } ] } ] }, { value: 'Miss14', children: [ { value: 'Hit4' } ] } ]
          .as-console-wrapper {max-height: 100% !important; top: 0}
          &lt;script src="https://bundle.run/object-scan@16.0.0"&gt;&lt;/script&gt;

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

          【讨论】:

            【解决方案9】:

            正在寻找另一种方法来解决这个问题而不直接改变 children 数组并想出了这个:

            const input = [
              {
                value: 'Miss1',
                children: [
                  { value: 'Miss2' },
                  { value: 'Hit1', children: [ { value: 'Miss3' } ] }
                ]
              },
              {
                value: 'Miss4',
                children: [
                  { value: 'Miss5' },
                  { value: 'Miss6', children: [ { value: 'Hit2' } ] }
                ]
              },
              {
                value: 'Miss7',
                children: [
                  { value: 'Miss8' },
                  { value: 'Miss9', children: [ { value: 'Miss10' } ] }
                ]
              },
              {
                value: 'Hit3',
                children: [
                  { value: 'Miss11' },
                  { value: 'Miss12', children: [ { value: 'Miss13' } ] }
                ]
              },
              {
                value: 'Miss14',
                children: [
                  { value: 'Hit4' },
                  { value: 'Miss15', children: [ { value: 'Miss16' } ] }
                ]
              },
            ];
            
            const filtered = input.reduce(function fr(acc, curr) {
              
              if (curr.children) {
                const children = curr.children.reduce(fr, []);
                if (children.length) {
                    return acc.concat({ ...curr, children: children });
                }
              }
              if (curr.value.includes('Hit')) {
                return acc.concat(curr);
              } else {
                return acc;
              }
            }, []);
            
            console.log(filtered);

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-09-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-09-15
              相关资源
              最近更新 更多