同时对数组进行映射和筛选

333

我有一个对象数组,想要循环迭代来生成一个新的过滤数组。但同时,我需要根据一个参数过滤掉新数组中的某些对象。我正在尝试这样做:

function renderOptions(options) {
    return options.map(function (option) {
        if (!option.assigned) {
            return (someNewObject);
        }
    });   
}

这是一种好方法吗?还有更好的方法吗?我愿意使用任何库,比如lodash。


那么采用“Object.keys”方法呢?https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Object/keys - Julo0sS
使用 reduce: https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Array/reduce - Yves M.
4
.reduce().filter(...).map(...)更快,这一点已经被其他人提出来了。我设置了一个JSPerf测试来证明 https://dev59.com/ol8d5IYBdhLWcg3wiSnI#47877054 - Justin L.
18个回答

421

你应该使用Array.reduce来完成此任务。

var options = [
  { name: 'One', assigned: true }, 
  { name: 'Two', assigned: false }, 
  { name: 'Three', assigned: true }, 
];

var reduced = options.reduce(function(filtered, option) {
  if (option.assigned) {
     var someNewValue = { name: option.name, newProperty: 'Foo' }
     filtered.push(someNewValue);
  }
  return filtered;
}, []);

document.getElementById('output').innerHTML = JSON.stringify(reduced);
<h1>Only assigned options</h1>
<pre id="output"> </pre>

另外,reducer也可以是一个纯函数,像这样:


var reduced = options.reduce(function(result, option) {
  if (option.assigned) {
    return result.concat({
      name: option.name,
      newProperty: 'Foo'
    });
  }
  return result;
}, []);

5
对我来说,第一个参数 filtered 是一个对象。因此,filtered.push 对我来说是未定义的。 - Rahmathullah M
11
我也遇到了filtered是对象的问题。这是因为在reduce函数之后,我没有传入“初始值”——空数组([])。例如不正确的方式:var reduced = options.reduce(function(filtered, option) { ... });正确的方式是:var reduced = options.reduce(function(filtered, option) { ... }, []); - Jon Higgins
1
@Marko,我也引入了一个纯净的reducer。请看一下。 - thefourtheye
1
@Marko 这里不能使用 flatMap,因为它不允许我们进行过滤。除非我们在这种情况下返回一个空数组。但我认为这并不方便读者。 - thefourtheye
1
也许你对可读性是正确的,但返回空数组正是我所想的,这是完全合法的用例。 - Marko
显示剩余2条评论

190
自2019年以来,Array.prototype.flatMap是一个不错的选择。
options.flatMap(o => o.assigned ? [o.name] : []);

来自上面链接的 MDN 页面:

flatMap可以用作一种在映射过程中添加和删除项目(修改项目数量)的方法。换句话说,它允许您将许多项目映射到许多项目(通过单独处理每个输入项目),而不总是一对一。从这个意义上讲,它的工作方式类似于 filter 的相反操作。只需返回一个包含1个元素的数组以保留该项,返回包含多个元素的数组以添加项目,或返回一个空数组以删除该项。


3
这种方法在性能上与使用两个循环(通过单独调用array.filter和array.map)相比如何? - Simon Tran
1
我使用了jsbench.me和jsben.ch进行比较,得到了非常不同的结果。第一次使用flatMap时速度慢了5倍,第二次使用时速度快了2倍。所以我不知道如何对其进行基准测试。 - Trevor Dixon
4
也许是因为我有更多的面向对象编程经验,但是这个答案对我来说比“reduce”更直观。 - Daniel Kaplan
body.files.flatMap(file => file?.file ?? []) // 如果您不需要三元运算符,可以更简洁 - Stefan T
1
使用ES6解构更加简洁:options.flatMap(({assigned, name}) => assigned ? [name] : []); - davetapley

70

使用ES6可以非常简洁地实现:

options.filter(opt => !opt.assigned).map(opt => someNewObject)


3
@hogan,但这是对原始查询的正确答案(可能不是最优解)。 - vikramvi
1
@vikramvi 谢谢您的留言。问题是,我们可以用很多种方式实现相同的事情,而我更喜欢最好的那种。 - hogan
3
这并不完全正确,因为在这里您丢失了值最初所处的索引位置的信息,而当运行map函数时,该信息可能非常有用。 - Luis Gurmendez
3
这种方式很好,但这个问题是“同时对数组进行映射和过滤”的操作。最佳方法是使用options.reduce(res,options....。回答@Zuker。 - hassan khademi

62

使用 reduce,卢克!

function renderOptions(options) {
    return options.reduce(function (res, option) {
        if (!option.assigned) {
            res.push(someNewObject);
        }
        return res;
    }, []);   
}

21

我想留下评论,但是我没有足够的声望。对 Maxim Kuzmin 很好的回答进行一点小改进,以使其更加高效:

const options = [
  { name: 'One', assigned: true }, 
  { name: 'Two', assigned: false }, 
  { name: 'Three', assigned: true }, 
];

const filtered = options
  .reduce((result, { name, assigned }) => assigned ? result.push(name) && result : result, []);

console.log(filtered);

解释

与其在每次迭代中反复传递整个结果,我们只是将其附加到数组中,并且仅在实际插入值时才这样做。


1
.concat() 返回一个新的数组,它实际上与将旧数组和新项展开到新数组中相同,MDN 在此。您想要使用 .push() 来“追加”,就像您所描述的那样。 - MarceDev
@MarceDev 哦,你说得对,谢谢。我会更新我的答案来做到这一点。语法会变得有点奇怪,因为push不会返回数组。 - matsve

20

在这里,使用ES6的精巧的扩展语法进行一行reduce的操作!

var options = [
  { name: 'One', assigned: true }, 
  { name: 'Two', assigned: false }, 
  { name: 'Three', assigned: true }, 
];

const filtered = options
  .reduce((result, {name, assigned}) => [...result, ...assigned ? [name] : []], []);

console.log(filtered);


2
非常好的@Maxim!我点赞了这个!但是...每添加一个元素,它都必须将result中的所有元素扩展开来...就像filter(assigned).map(name)解决方案。 - 0zkr PM
4
不错。我花了一点时间才意识到 ...assigned ? [name] : [] 的含义 - 如果写成 ...(assigned ? [name] : []) 可能更易读。 - johansenja
这似乎不太高效... - zr0gravity7

8

在某些情况下,使用forEach更容易(或同样容易)。

var options = [
  { name: 'One', assigned: true }, 
  { name: 'Two', assigned: false }, 
  { name: 'Three', assigned: true }, 
];

var reduced = []
options.forEach(function(option) {
  if (option.assigned) {
     var someNewValue = { name: option.name, newProperty: 'Foo' }
     reduced.push(someNewValue);
  }
});

document.getElementById('output').innerHTML = JSON.stringify(reduced);
<h1>Only assigned options</h1>
<pre id="output"> </pre>

然而,如果有一个将 mapfilter 函数结合起来的 malter()fap() 函数会更好。它将像一个过滤器一样工作,但不是返回 true 或 false,而是返回任何对象或 null/undefined。


6
你可能需要检查一下你的梗图... ;-) - Arel
3
但是这样你就不能炫耀你那一行只有你自己能理解的133t JS了。 - Half_Duplex
2
这些名称非常重要,你应该向TC39提交提案。 - Lukasz Matysiak

7
使用 Array.prototype.filter 方法:
function renderOptions(options) {
    return options.filter(function(option){
        return !option.assigned;
    }).map(function (option) {
        return (someNewObject);
    });   
}

2
如果您想要的值是在过滤过程中计算出来的,那么这种方法并不是很有效,因为您需要复制代码的一部分。 - Lucretiel

6
我用以下方法优化答案:
  1. if (cond) { stmt; } 重写为 cond && stmt;
  2. 使用 ES6 箭头函数
我将呈现两种解决方案,一种使用 forEach ,另一种使用 reduce 解决方案1:使用forEach 该解决方案通过使用 forEach 迭代每个元素来工作。 然后,在 forEach 循环的主体中,我们有条件作为过滤器,它确定我们是否要向结果数组附加内容。

const options = [
  { name: 'One', assigned: true }, 
  { name: 'Two', assigned: false }, 
  { name: 'Three', assigned: true }, 
];
const reduced = [ ];
options.forEach(o => {
  o.assigned && reduced.push( { name: o.name, newProperty: 'Foo' } );
} );
console.log(reduced);

解决方案2:使用reduce

这个解决方案使用Array.prototype.reduce而不是forEach来迭代数组。它认识到reduce既有初始化器又有内置的循环机制。除此之外,这个解决方案和forEach的解决方案基本相同,所以,区别在于语法上的差异。

const options = [
  { name: 'One', assigned: true }, 
  { name: 'Two', assigned: false }, 
  { name: 'Three', assigned: true }, 
];
const reduced = options.reduce((a, o) => {
  o.assigned && a.push( { name: o.name, newProperty: 'Foo' } );
  return a;
}, [ ] );
console.log(reduced);

我让你自己决定采用哪个解决方案。


1
你为什么要使用 cond && stmt;?这种写法更难阅读,而且没有任何好处。 - jlh

4
我已经将这些伟大的答案转化为实用函数,并想分享它们:
示例:仅过滤奇数并递增
例如:[1, 2, 3, 4, 5] - 过滤 -> [1, 3, 5] - 映射 -> [2, 4, 6]
通常您会使用filter和map来执行此操作。
const inputArray = [1, 2, 3, 4, 5];
const filterOddPlusOne = inputArray.filter((item) => item % 2).map((item) => item + 1); // [ 2, 4, 6 ]

使用reduce函数

const filterMap = <TSource, TTarget>(
  items: TSource[],
  filterFn: (item: TSource) => boolean,
  mapFn: (item: TSource) => TTarget
) =>
  items.reduce((acc, cur): TTarget[] => {
    if (filterFn(cur)) return [...acc, mapFn(cur)];
    return acc;
  }, [] as TTarget[]);

使用flatMap

const filterMap = <TSource, TTarget>(
  items: TSource[],
  filterFn: (item: TSource) => boolean,
  mapFn: (item: TSource) => TTarget
) => items.flatMap((item) => (filterFn(item) ? [mapFn(item)] : []));

用法(对于reduceflatMap解决方案相同):

const inputArray = [1, 2, 3, 4, 5];
const filterOddPlusOne = filterMap(
  inputArray,
  (item) => item % 2, // Filter only odd numbers
  (item) => item + 1 // Increment each number
); // [ 2, 4, 6 ]

JavaScript 版本

上述代码为 TypeScript,但问题要求使用 JavaScript。因此,我已经为您删除了所有泛型和类型:

const filterMap = (items, filterFn, mapFn) =>
  items.reduce((acc, cur) => {
    if (filterFn(cur)) return [...acc, mapFn(cur)];
    return acc;
  }, []);

const filterMap = (items, filterFn, mapFn) =>
  items.flatMap((item) => (filterFn(item) ? [mapFn(item)] : []));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接