Mapuj i filtruj tablicę w tym samym czasie

155

Mam tablicę obiektów, które chcę iterować, aby utworzyć nową filtrowaną tablicę. Ale także muszę odfiltrować niektóre obiekty z nowej tablicy w zależności od parametru. Próbuję tego:

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

Czy to dobre podejście? Czy jest lepsza metoda? Jestem otwarty na korzystanie z dowolnej biblioteki, takiej jak lodash.

Daniel Calderon Mori
źródło
a co z podejściem „object.keys”? developer.mozilla.org/fr/docs/Web/JavaScript/Reference/…
Julo0sS
3
.reduce()jest zdecydowanie szybszy niż zrobienie .filter(...).map(...)tego, co sugerowałem gdzie indziej. Skonfigurowałem test JSPerf, aby zademonstrować stackoverflow.com/a/47877054/2379922
Justin L.

Odpowiedzi:

219

Powinieneś użyć Array.reducedo tego.

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>


Alternatywnie, reduktor może być czystą funkcją, jak ta

var reduced = options.reduce(function(result, option) {
  if (option.assigned) {
    return result.concat({
      name: option.name,
      newProperty: 'Foo'
    });
  }
  return result;
}, []);
na czworo
źródło
2
Dla mnie pierwszy argument filteredto obiekt. więc filtered.pushjest dla mnie niezdefiniowane.
Rahmathullah M
4
Miałem też problem z filteredbyciem obiektem. To dlatego, że nie przekazałem „wartości początkowej” - pustej tablicy ( []) po funkcji redukuj. np. niepoprawne var reduced = options.reduce(function(filtered, option) { ... }); poprawne var reduced = options.reduce(function(filtered, option) { ... }, []);
Jon Higgins
Nie ma powodu do reducetego, równie dobrze możesz go użyć, forEachponieważ w każdej iteracji zmieniasz tablicę, filtereda zatem nie jest to czysta funkcjonalność. Byłoby to wydarzenie bardziej czytelne i zwięzłe.
Marko
1
@Marko Wprowadziłem też czysty reduktor. PTAL.
thefourtheye
Tak, to jest teraz czyste. +1 za twój wysiłek. Nie, żebym był zwolennikiem programowania funkcjonalnego, wręcz przeciwnie, po prostu nie widziałem sensu :) Ale tak naprawdę skorzystałem z twojego triku po obejrzeniu go, bo tak się przydał w danej sytuacji. Ale w tym zadaniu możesz rzucić okiem na funkcję flatMap, myślę, że stała się standardem po udzieleniu odpowiedzi (również z tego powodu może być nieobsługiwana przez niektóre przeglądarki). Powinien być bardziej wydajny, ponieważ łączenie tablic w ten sposób powoduje, że O (n ^ 2) zadanie jest poza O (n) zadanie.
Marko,
43

Użyj redukcji, Luke!

function renderOptions(options) {
    return options.reduce(function (res, option) {
        if (!option.assigned) {
            res.push(someNewObject);
        }
        return res;
    }, []);   
}
Zuker
źródło
30

Od 2019 roku Array.prototype.flatMap jest dobrą opcją.

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

Z powyższej strony MDN:

flatMapmoże służyć jako sposób na dodawanie i usuwanie elementów (modyfikowanie liczby elementów) na mapie. Innymi słowy, umożliwia mapowanie wielu elementów do wielu elementów (poprzez obsługę każdego elementu wejściowego osobno) zamiast zawsze jeden do jednego. W tym sensie działa jak przeciwieństwo filtra. Po prostu zwróć tablicę 1-elementową, aby zachować element, tablicę wieloelementową, aby dodać elementy, lub tablicę 0-elementową, aby usunąć element.

Trevor Dixon
źródło
5
Świetne rozwiązanie! Ale czytelnicy powinni pamiętać, że flatMapzostał niedawno wprowadzony, a jego obsługa przeglądarki jest ograniczona . Możesz użyć oficjalnego polyfill / shims : flatMap i flat .
Arel
24

Z ES6 możesz to zrobić bardzo krótko:

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

Natividad Lara Diaz
źródło
25
Spowoduje to wykonanie dwóch pętli, w zależności od tego, jakie filtry zwrotne mogą nadal zajmować pamięć.
hogan
1
@hogan, ale to jest poprawna odpowiedź (może nie być optymalnym rozwiązaniem) na pierwotne zapytanie
vikramvi
1
@vikramvi dziękuję za twoją notatkę. Chodzi o to, że możemy osiągnąć to samo na wiele sposobów, a ja wolałbym najlepszy.
hogan
10

Jedna linia reducez fantazyjną składnią rozszerzania ES6 jest tutaj!

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
źródło
1
Naprawdę miło @Maxim! Głosuję za to! Ale ... na każdym dodanym elemencie musi rozłożyć wszystkie elementy w result... jest jak filter(assigned).map(name)rozwiązanie
0zkr PM
Niezłe. Zajęło mi ...assigned ? [name] : []...(assigned ? [name] : [])
chwilę, zanim
5

Zrobiłbym komentarz, ale nie mam wymaganej reputacji. Małe ulepszenie w stosunku do bardzo dobrej odpowiedzi Maxima Kuzmina, aby uczynić ją bardziej wydajną:

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

Wyjaśnienie

Zamiast rozprowadzać cały wynik w kółko dla każdej iteracji, dopisujemy tylko do tablicy i tylko wtedy, gdy faktycznie istnieje wartość do wstawienia.

matsve
źródło
3

Użyj samego Array.prototy.filter

function renderOptions(options) {
    return options.filter(function(option){
        return !option.assigned;
    }).map(function (option) {
        return (someNewObject);
    });   
}
AmmarCSE
źródło
2

W pewnym momencie, czy nie jest łatwiej (lub równie łatwo) używać pliku 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>

Byłoby jednak miło, gdyby istniała funkcja malter()lub fap()łącząca funkcje mapi filter. Działałby jak filtr, z tą różnicą, że zamiast zwracać wartość true lub false, zwróciłby dowolny obiekt lub wartość null / undefined.

Daniel
źródło
Może zechcesz sprawdzić swoje memy ... ;-)
Arel
2

Zoptymalizowałem odpowiedzi, uwzględniając następujące punkty:

  1. Przepisując if (cond) { stmt; }jakocond && stmt;
  2. Użyj funkcji strzałek ES6

Przedstawię dwa rozwiązania, jedno wykorzystujące forEach , drugie wykorzystujące redukuj :

Rozwiązanie 1: Korzystanie z 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);

Rozwiązanie 2: Korzystanie z programu Redukcja

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

Decyzja o wyborze rozwiązania pozostawiam Tobie.

Stephen Quan
źródło
1
Dlaczego u licha miałbyś użyć cond && stmt;? Jest to dużo trudniejsze do odczytania i nie daje żadnych korzyści.
jlh
1

Używając redukuj, możesz to zrobić w jednej funkcji Array.prototype. Spowoduje to pobranie wszystkich liczb parzystych z tablicy.

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>

Możesz użyć tej samej metody i uogólnić ją dla swoich obiektów, w ten sposób.

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

arr będzie teraz zawierać przefiltrowane obiekty.

Bhargav Ponnapalli
źródło
0

Bezpośrednie użycie .reducemoże być trudne do odczytania, dlatego polecam utworzenie funkcji, która generuje reduktor:

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

Użyj go w ten sposób:

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
źródło