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.

Daniel Calderon Mori
fonte
que tal uma abordagem "object.keys"? developer.mozilla.org/fr/docs/Web/JavaScript/Reference/…
Julo0sS
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;
}, []);
thefourtheye
fonte
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;
    }, []);   
}
Zuker
fonte
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.

Trevor Dixon
fonte
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)

Natividad Lara Diaz
fonte
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);

Maxim Kuzmin
fonte
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.

matsve
fonte
3

Use o próprio Array.prototy.filter

function renderOptions(options) {
    return options.filter(function(option){
        return !option.assigned;
    }).map(function (option) {
        return (someNewObject);
    });   
}
AmmarCSE
fonte
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.

Daniel
fonte
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.

Stephen Quan
fonte
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.

Bhargav Ponnapalli
fonte
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'];
Quelklef
fonte