Mapeie e filtre uma matriz ao mesmo tempo


155

Eu tenho uma matriz de objetos que quero iterar para produzir uma nova matriz filtrada. Mas também preciso filtrar alguns objetos da nova matriz, dependendo de um parâmetro. Estou tentando isso:

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

Essa é uma boa abordagem? Há um método melhor? Estou aberto a usar qualquer biblioteca como o lodash.




3
.reduce()é definitivamente mais rápido do que fazer um .filter(...).map(...)que eu vi sugerido em outro lugar. Criei um teste para demonstrar JSPerf stackoverflow.com/a/47877054/2379922
Justin L.

Respostas:


218

Você deve usar Array.reducepara isso.

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>


Como alternativa, o redutor pode ser uma função pura, como esta

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

2
Para mim, o primeiro argumento, filteredé um objeto. então filtered.pushé indefinido para mim.
Rahmathullah M

4
Eu também tive um problema em filteredser um objeto. Isso ocorreu porque eu não estava passando o "valor inicial" - a matriz vazia ( []) após a função de redução. por exemplo, incorreto var reduced = options.reduce(function(filtered, option) { ... }); correto var reduced = options.reduce(function(filtered, option) { ... }, []);
Jon Higgins

Não há razão para reduceaqui, você poderia usar da mesma maneira, forEachpois em todas as iterações você altera o array filterede, portanto, não é totalmente funcional. Seria um evento mais legível e compacto.
Marko

1
@ Marko eu introduzi um redutor puro também. PTAL.
thefourtheye

Sim, isso é puro agora. +1 pelo seu esforço. Não que eu seja um defensor da programação funcional pura, longe disso, eu simplesmente não conseguia entender o ponto :) Mas, na verdade, eu usei o seu truque depois de vê-lo, porque ele foi muito útil em uma determinada situação. Mas para esta tarefa, talvez você queira dar uma olhada na função flatMap, acho que ela entrou no padrão depois que você respondeu (também por esse motivo pode não ser suportado por alguns navegadores). Deveria ter mais desempenho, pois concatenar matrizes como essa faz da tarefa O (n ^ 2) fora da tarefa O (n).
Marko

43

Use reduzir, Luke!

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

30

Desde 2019, o Array.prototype.flatMap é uma boa opção.

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

Na página MDN vinculada acima:

flatMappode ser usado como uma maneira de adicionar e remover itens (modificar o número de itens) durante um mapa. Em outras palavras, ele permite mapear muitos itens para muitos itens (manipulando cada item de entrada separadamente), em vez de sempre individualmente. Nesse sentido, funciona como o oposto do filtro. Basta retornar uma matriz de 1 elemento para manter o item, uma matriz de vários elementos para adicionar itens ou uma matriz de 0 elementos para remover o item.


5
Ótima solução! Porém, os leitores devem ter em mente que flatMapfoi recentemente apresentado e seu suporte ao navegador é limitado . Você pode usar o polyfill / calços oficiais: flatMap e flat .
Arel

24

Com o ES6, você pode fazer isso muito brevemente:

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


25
Isso fará dois loops, dependendo do retorno do filtro ainda pode consumir memória.
Hogan #

1
@hogan mas esta é a resposta correta (pode não ser a solução ideal) para a consulta original
vikramvi

1
@vikramvi obrigado pela sua nota. O problema é que podemos conseguir as mesmas coisas de várias maneiras e eu prefiro a melhor.
Hog

10

Uma linha reducecom a sintaxe de propagação sofisticada do ES6 está aqui!

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


1
Muito bom @Maxim! Eu votei isso! Mas ... em cada elemento adicionado, ele tem que espalhar todos os elementos em result... é como filter(assigned).map(name)solução #
0zkr PM

Agradável. Levei um segundo para perceber o que estava acontecendo ...assigned ? [name] : []- pode ser mais legível como...(assigned ? [name] : [])
johansenja

5

Eu faria um comentário, mas não tenho a reputação necessária. Uma pequena melhoria na resposta muito boa de Maxim Kuzmin para torná-lo mais eficiente:

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

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

console.log(filtered);

Explicação

Em vez de espalhar o resultado inteiro repetidamente para cada iteração, apenas anexamos à matriz e somente quando há realmente um valor a ser inserido.


3

Use o próprio Array.prototy.filter

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

2

Em algum momento, não é mais fácil (ou tão fácil) usar um 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>

No entanto, seria bom se houvesse uma função malter()ou fap()que combine as funções mape filter. Funcionaria como um filtro, exceto que, em vez de retornar verdadeiro ou falso, retornaria qualquer objeto ou um nulo / indefinido.


Você pode querer verificar seus memes ... ;-)
Arel

2

Otimizei as respostas com os seguintes pontos:

  1. Reescrevendo if (cond) { stmt; }comocond && stmt;
  2. Use as funções de seta do ES6

Vou apresentar duas soluções, uma usando forEach , a outra usando o método reduzir :

Solução 1: Usando o forEach

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

Solução 2: usando reduzir

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

Deixo a você decidir qual solução escolher.


1
Por que diabos você usaria cond && stmt;? Isso é muito mais difícil de ler e não oferece nenhum benefício.
JLH

1

Usando reduzir, você pode fazer isso em uma função Array.prototype. Isso buscará todos os números pares de uma matriz.

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>

Você pode usar o mesmo método e generalizá-lo para seus objetos, assim.

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

arr agora conterá os objetos filtrados.


0

O uso direto de .reducepode ser difícil de ler, por isso recomendo criar uma função que gere o redutor para você:

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

Use-o assim:

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'];
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.