递归筛选对象数组

Recursively filter array of objects

提问人:Nathan Power 提问时间:7/1/2016 更新时间:11/7/2021 访问量:44893

问:

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

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' } ] }
    ]
  },
];

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

我需要一个返回新数组的函数,这样:

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

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

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

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

输出应如下所示:

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' },
    ]
  }
];

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

JavaScript 数组 递归 过滤器

评论

0赞 7/1/2016
所以你似乎是在说,最终只有最外层数组中的对象才会包含在结果中,因为如果嵌套在匹配中的任何对象,它的整个结构都会被保留。在这种情况下,似乎您只需要 ,其中函数进行递归调用,该调用在最终找到匹配项时返回。input.filter(function(o) {...})true
0赞 4castle 7/1/2016
@squint 不完全是。请注意如何从Miss2Miss1
0赞 7/1/2016
@4castle:说得好,但我想知道为什么其他人没有被删除。我错过了什么?
0赞 4castle 7/1/2016
@squint 非常仔细地阅读他们的标准。输出与他们所说的完全一样。
0赞 7/1/2016
啊,所有热门歌曲的后代......

答:

15赞 jj689 7/1/2016 #1

这里有一个函数可以做你要找的东西。从本质上讲,它将测试每个项目是否匹配,然后递归调用其 filter 。此外,使用该对象不会更改基础对象。arrchildrenObject.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;
}

评论

2赞 Nathan Power 7/1/2016
谢谢,这种方法实际上让我更清楚地知道我哪里出了问题
0赞 Raphael Pinel 6/1/2021
我认为将其称为“过滤器”是一个令人困惑的问题,因为“过滤器”已经是 Javascript 中的数组方法。我们可以称之为“搜索”
68赞 4 revs, 2 users 99%user1106925 #2

正如我在上面的评论中描述的那样,使用和进行递归调用基本上是您所需要的。在返回之前,只需使用递归调用的结果更新每个属性即可。.filter().children

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

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 上是 ES7,因此可能需要针对旧版浏览器进行修补。您可以使用传统来代替它。.includes().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
})

虽然它有点难以阅读。

评论

3赞 4castle 7/1/2016
它改变了原始内容。您可能首先要使用 .另外,我感觉这是服务器端 JS。inputJSON.parse(JSON.stringify(input))
0赞 7/1/2016
最简单的方法是从一开始就克隆整个结构。如果每个对象确实只有 2 个属性,那么首先对每个对象执行 a 应该足够简单。.map().filter()
1赞 7/1/2016
@guest271314:我想你可能和我原来一样想,即使某样东西有一个“命中”的后代,它的所有后代都会被保留下来。这实际上也很容易,但从显示的描述和输出来看,唯一保留的失误要么是“命中”的后代,要么是命中的祖先。
1赞 Sapnesh Naik 3/5/2020
您能解释一下为什么需要吗?lengthreturn (o.children = o.children.filter(f)).length
1赞 cdehaan 10/22/2021
@Sapnesh Naik 如果作为“命中”的子项的长度为 0,则此 0 将返回为假值,因此筛选器将对其进行修剪。相反,如果找到“命中”,则长度将大于 0,因此它将返回为 true,并且过滤器将包含/返回它。
2赞 Zohaib Ijaz 7/1/2016 #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);
0赞 Yuri Gor 1/16/2019 #4

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

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

这是针对您的情况的完整测试

1赞 sudheer nunna 2/17/2020 #5

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))

1赞 Malik Bagwala 10/29/2020 #6

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

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));

评论

0赞 Scott Sauyet 10/29/2020
只有当其他问题真正解决了手头的问题时,才请提交现有的解决方案;该问题与 S 或 无关。如果有类比,请做。此外,您的简介不足以阻止它成为纯代码解决方案;不要发布这些。最后,在复活一个旧问题时,请确保你有新的东西要添加。routeisAuthorized.
0赞 Malik Bagwala 10/30/2020
@ScottSauyet很抱歉缺少上下文,我添加了必要的更改,我觉得与其他解决方案相比,这个特定的解决方案更具可读性和合理性。
0赞 Scott Sauyet 10/30/2020
@mailk:它仍然不起作用。第 3 行缺少一个,但即使修复了,您仍然有一个未定义的变量。在编写 JS/HTML/CSS 解决方案时,如果可能的话,请创建一个代码片段来演示它的工作原理。)route
0赞 Malik Bagwala 10/31/2020
@ScottSauyet对不起......添加了必要的更改。而且它似乎工作正常
0赞 Scott Sauyet 11/1/2020
@mailk:问题中有请求的输出。这与它不匹配,甚至不接近。公认的答案和这里的其他几个答案确实如此。我认为如果你要迟到,你真的应该得到一个符合要求的解决方案。
1赞 Mulan 10/31/2020 #7

Array.prototype.flatMap非常适合递归过滤。与 和 类似,using 不会修改原始输入 -mapfilterreduceflatMap

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"
      }
    ]
  }
]
0赞 vincent 12/31/2020 #8

这是使用对象扫描的不同类型的解决方案。

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

这主要是一项学术练习,因为递归方法更干净、更快速。但是,如果需要进行其他处理,或者堆栈深度错误是一个问题,则此答案可能会很有趣。

// 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}
<script src="https://bundle.run/[email protected]"></script>

免责声明:我是对象扫描的作者

0赞 bstiffler582 11/7/2021 #9

正在寻找另一种在不直接改变子数组的情况下解决这个问题的方法,并提出了这个:

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);