【问题标题】:Map and filter an array at the same time同时映射和过滤数组
【发布时间】:2025-11-25 02:10:02
【问题描述】:

我有一个对象数组,我想对其进行迭代以生成一个新的过滤数组。而且,我需要根据参数从新数组中过滤掉一些对象。我正在尝试这个:

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

这是一个好方法吗?有没有更好的方法?我愿意使用任何库,例如 lodash。

【问题讨论】:

标签: javascript arrays


【解决方案1】:

您应该为此使用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;
}, []);

【讨论】:

  • 对我来说,第一个参数filtered 是一个对象。所以 filtered.push 对我来说是未定义的。
  • 我也有一个问题,filtered 是一个对象。这是因为我没有传入“初始值”——reduce 函数之后的空数组 ([])。例如不正确var reduced = options.reduce(function(filtered, option) { ... }); 正确var reduced = options.reduce(function(filtered, option) { ... }, []);
  • @Marko 我还介绍了一个纯减速器。 PTAL。
  • Yap 现在很纯净。 +1 为您的努力。并不是说我是纯粹的函数式编程倡导者,远非如此,我只是看不出重点:) 但实际上我在看到它之后就使用了你的技巧,因为它在给定的情况下非常方便。但是对于这个任务,您可能想看看 flatMap 功能,我认为它在您做出回答后成为标准(也因为这个原因,某些浏览器可能不支持)。它应该更高效,因为像这样连接数组会使 O(n^2) 任务脱离 O(n) 任务。
  • @Marko flatMap 不能在这里使用,因为它不允许我们过滤。除非,在这种情况下我们返回一个空数组。我相信这对读者来说并不容易。
【解决方案2】:

自 2019 年以来,Array.prototype.flatMap 是不错的选择。

options.flatMap(o => o.assigned ? [o.name] : []);

来自上面链接的 MDN 页面:

flatMap 可以用作添加和删除项目的一种方式(修改 地图期间的项目数)。换句话说,它允许您映射 许多项目对许多项目(通过分别处理每个输入项目), 而不是总是一对一的。从这个意义上说,它的工作原理类似于 过滤器的对面。只需返回一个 1 元素数组来保留该项目, 用于添加项目的多元素数组,或用于删除的 0 元素数组 项目。

【讨论】:

  • 这在性能方面与使用 2 个循环(分别调用 array.filter 和 array.map)相比如何?
  • 我使用 jsbench.me 和 jsben.ch 进行了比较,得到了非常不同的结果。第一次 flatMap 慢了 5 倍,第二次快了 2 倍。所以我不知道如何对其进行基准测试。
【解决方案3】:

使用 reduce,卢克!

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

【讨论】:

    【解决方案4】:

    使用 ES6,你可以做的很短:

    options.filter(opt =&gt; !opt.assigned).map(opt =&gt; someNewObject)

    【讨论】:

    • @hogan 但这是原始查询的正确答案(可能不是最佳解决方案)
    • @vikramvi 谢谢你的来信。问题是,我们可以通过多种方式实现相同的目标,我更喜欢最好的方式。
    • 这并不完全正确,因为在这里您丢失了初始定位值的索引,并且该信息在运行地图 fn 时非常有用。
    • 这种方式很好,但这个问题是同时映射和过滤数组最好的方式是options.reduce(res,options.... answer @Zuker
    【解决方案5】:

    一行reduce 与 ES6 花哨spread syntax 在这里!

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

    【讨论】:

    • 真的很好@Maxim!我赞成这个!但是...在添加的每个元素上,它必须传播result...中的所有元素...就像filter(assigned).map(name)解决方案
    • 不错的一个。我花了一秒钟才意识到...assigned ? [name] : [] 发生了什么——它可能更易读为...(assigned ? [name] : [])
    【解决方案6】:

    我会发表评论,但我没有所需的声誉。对 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);

    说明

    我们不是在每次迭代中一遍又一遍地传播整个结果,而是只追加到数组中,并且只有在实际有一个值要插入时。

    【讨论】:

    • .concat() 返回一个新数组,它实际上与将旧数组和新项分散到一个新数组中相同,MDN here。您想按照您的描述使用.push()“追加”。
    • @MarceDev 啊,你是对的,谢谢。将更新我的答案来做到这一点。然后语法变得有点奇怪,因为 push 不返回数组。
    【解决方案7】:

    在某些时候,使用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>

    但是,如果有一个 malter()fap() 函数结合了 mapfilter 函数,那就太好了。它会像过滤器一样工作,除了返回 true 或 false 之外,它会返回任何对象或 null/undefined。

    【讨论】:

    • 你可能想检查你的模因... ;-)
    • 但是你不会炫耀你的 1 行 133t JS,除了你之外没有人理解。
    • 这些名字是金,你应该向 TC39 提交提案
    【解决方案8】:

    使用Array.prototy.filter 本身

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

    【讨论】:

    • 如果你想要的值是在做过滤器的过程中计算出来的,这样就不太好用了;你必须复制那部分代码。
    【解决方案9】:

    我用以下几点优化了答案:

    1. if (cond) { stmt; } 重写为cond &amp;&amp; stmt;
    2. 使用 ES6 Arrow Functions

    我将介绍两种解决方案,一种使用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);

    由您决定采用哪种解决方案。

    【讨论】:

    • 你到底为什么要使用cond &amp;&amp; stmt;?这更难阅读,而且根本没有任何好处。
    【解决方案10】:

    使用 reduce,您可以在一个 Array.prototype 函数中执行此操作。这将从数组中获取所有偶数。

    var arr = [1,2,3,4,5,6,7,8];
    
    var brr = arr.reduce((c, n) => {
      if (n % 2 !== 0) {
        return c;
      }
      c.push(n);
      return c;
    }, []);
    
    document.getElementById('mypre').innerHTML = brr.toString();
    <h1>Get all even numbers</h1>
    <pre id="mypre"> </pre>

    您可以使用相同的方法并将其推广到您的对象,就像这样。

    var arr = options.reduce(function(c,n){
      if(somecondition) {return c;}
      c.push(n);
      return c;
    }, []);
    

    arr 现在将包含过滤后的对象。

    【讨论】:

      【解决方案11】:

      直接使用.reduce 可能难以阅读,因此我建议创建一个为您生成reducer 的函数:

      function mapfilter(mapper) {
        return (acc, val) => {
          const mapped = mapper(val);
          if (mapped !== false)
            acc.push(mapped);
          return acc;
        };
      }
      

      像这样使用它:

      const words = "Map and filter an array #javascript #arrays";
      const tags = words.split(' ')
        .reduce(mapfilter(word => word.startsWith('#') && word.slice(1)), []);
      console.log(tags);  // ['javascript', 'arrays'];
      

      【讨论】:

        【解决方案12】:

        您可以使用Array.reduce 与箭头函数是单行代码

        const options = [
          { name: 'One', assigned: true }, 
          { name: 'Two', assigned: false }, 
          { name: 'Three', assigned: true }, 
        ];
        
        const reduced = options.reduce((result, option) => option.assigned ? result.concat({ name: option.name, newProperty: 'Foo' }) : result, []);
        
        document.getElementById('output').innerHTML = JSON.stringify(reduced);
        <h1>Only assigned options</h1>
        <pre id="output"> </pre>

        【讨论】:

          【解决方案13】:

          我已将这些出色的答案转换为实用函数,我想分享它们:

          示例:仅过滤奇数并将其递增

          • 例如[1, 2, 3, 4, 5] -filter-> [1, 3, 5] -map-> [2, 4, 6]

          通常你会用 filtermap 这样做

          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)] : []));
          

          【讨论】:

            【解决方案14】:

            同时执行filter + map 的最有效方法是将数据作为通用可迭代对象处理,并同时执行这两项操作。在这种情况下,您最多只能浏览一次数据。

            下面的示例使用iter-ops 库,并且正是这样做的:

            import {pipe, filter, map} from 'iter-ops';
            
            const i = pipe(
                inputArray,
                filter(value => value === 123), // filter on whatever key you want
                map(value => /* any mapping here*/) // remap data as you like
            );
            
            // i = iterable that can be processed further;
            
            console.log([...i]); //=> list of new objects
            

            上面,我说的是at most,因为如果你对可迭代的结果应用进一步的逻辑,比如限制映射项的数量,你最终会迭代你的对象列表甚至少于一次:

            const i = pipe(
                inputArray,
                filter(value => value === 123), // filter on whatever key you want
                map(value => /* any mapping here*/), // remap as you like
                take(10) // take up to 10 items only
            );
            

            在上面,我们进一步限制了迭代,一旦生成了 10 个结果项就停止,因此我们对数据的迭代次数少于一次。这已经是最有效的了。

            更新

            我被要求补充为什么这个解决方案比reduce 更有效的答案,所以这里是......

            Array 的reduce 是一个有限运算,它遍历完整的数据集,以产生结果。因此,当您需要对该输出数据进行进一步处理时,您最终会生成一个新的迭代序列,依此类推。

            当您有一个复杂的业务逻辑要应用于序列/可迭代对象时,链接该逻辑总是更有效,同时只对序列进行一次迭代。在许多情况下,您最终会对一个序列进行复杂的处理,甚至一次都没有检查完整的数据集。这就是可迭代数据处理的效率。

            附:我是上述库的作者。

            【讨论】:

              【解决方案15】:

              嘿,我刚刚从事这个项目,想在 MDM 文档上分享我基于 Array.prototype.flatMap() 的解决方案:

               const places = [
               {latitude: 40,longitude: 1}, {latitude:41, longitude:2}, {latitude:44, longitude:2}, {latitude:NaN, longitude:NaN },{ latitude:45, longitude:4},{latitude:48, longitude:3}, {latitude:44, longitude:5}, {latitude:39, longitude:13}, {latitude:40, longitude:8}, {latitude:38, longitude:4}
               ]
               
               let items = places?.map((place) => [
                  {
                    latitude: (place.latitude),
              
                    longitude:(place.longitude),
                  },
                ]);
                 console.log("Items: ", items);
                 
                 //Remove elements with NaN latitude and longitude
                 
                  let newItems = places?.flatMap((o) =>
                  Number(o.longitude, o.latitude)
                    ? { lng: Number(o.longitude), lat: Number(o.latitude) }
                    : []
                ); 
                
               console.log("Coordinates after NaN values removed: ", newItems);
              一种

              【讨论】: