Jak pominąć element w .map ()?

417

Jak mogę pominąć element tablicy .map?

Mój kod:

var sources = images.map(function (img) {
    if(img.src.split('.').pop() === "json"){ // if extension is .json
        return null; // skip
    }
    else{
        return img.src;
    }
});

Zwróci to:

["img.png", null, "img.png"]
Ismail
źródło
18
Nie możesz, ale później możesz odfiltrować wszystkie wartości null.
Felix Kling
1
Dlaczego nie? Wiem, że używanie kontynuacji nie działa, ale dobrze byłoby wiedzieć, dlaczego (również uniknęłoby podwójnej pętli) - edytuj - w twoim przypadku nie możesz po prostu odwrócić warunku if i powrócić tylko, img.srcjeśli wynik podzielonego popu! = json?
GrayedFox
@GrayedFox Następnie niejawne undefinedzostanie wstawione do tablicy zamiast null. Nie tak lepiej ...
FZs

Odpowiedzi:

637

Po .filter()pierwsze:

var sources = images.filter(function(img) {
  if (img.src.split('.').pop() === "json") {
    return false; // skip
  }
  return true;
}).map(function(img) { return img.src; });

Jeśli nie chcesz tego robić, co nie jest nierozsądne, ponieważ wiąże się to z pewnymi kosztami, możesz użyć bardziej ogólnego .reduce(). Ogólnie możesz wyrazić .map()w kategoriach .reduce:

someArray.map(function(element) {
  return transform(element);
});

można zapisać jako

someArray.reduce(function(result, element) {
  result.push(transform(element));
  return result;
}, []);

Jeśli więc chcesz pominąć elementy, możesz to łatwo zrobić za pomocą .reduce():

var sources = images.reduce(function(result, img) {
  if (img.src.split('.').pop() !== "json") {
    result.push(img.src);
  }
  return result;
}, []);

W tej wersji kod .filter()z pierwszej próbki jest częścią .reduce()wywołania zwrotnego. Źródło obrazu jest wypychane na tablicę wyników tylko w przypadku, gdy zachowałoby to działanie filtru.

Pointy
źródło
21
Czy to nie wymaga dwukrotnego zapętlenia całej tablicy? Czy jest jakiś sposób, aby tego uniknąć?
Alex McMillan
7
@AlexMcMillan możesz użyć .reduce()i zrobić to wszystko w jednym przejściu, chociaż pod względem wydajności wątpię, aby zrobiłoby to znaczącą różnicę.
Pointy,
9
Z wszystkich tych negatywnych, „puste” wartości -Style ( null, undefined, NaNetc) Byłoby dobrze, gdybyśmy mogli wykorzystać jedną wewnątrz map()jako wskaźnik, że ten obiekt mapuje do niczego i powinny być pominięte. Często spotykam tablice, które chcę zmapować 98% (np. String.split()Pozostawiając na końcu pojedynczy, pusty ciąg, o który nie dbam). Dzięki za odpowiedź :)
Alex McMillan
6
@AlexMcMillan .reduce()to rodzaj funkcji „rób co chcesz”, ponieważ masz pełną kontrolę nad wartością zwracaną. Być może zainteresuje Cię doskonała praca Richa Hickeya w Clojure dotycząca koncepcji przetworników .
Pointy,
3
@vsync, z którym nie można pominąć elementu .map(). Możesz jednak użyć .reduce()zamiast tego, więc dodam to.
Pointy
25

Myślę, że najprostszym sposobem na pominięcie niektórych elementów z tablicy jest użycie metody filter () .

Korzystając z tej metody ( ES5 ) i składni ES6 , możesz napisać kod w jednym wierszu , a to zwróci to, co chcesz :

let images = [{src: 'img.png'}, {src: 'j1.json'}, {src: 'img.png'}, {src: 'j2.json'}];

let sources = images.filter(img => img.src.slice(-4) != 'json').map(img => img.src);

console.log(sources);

simhumileco
źródło
1
właśnie po to .filter()został stworzony
lawina 1
2
Czy jest to lepsze niż forEachwypełnianie go jednym przejściem zamiast dwóch?
wuliwong
1
Jak chcesz @wuliwong. Ale weź pod uwagę, że nadal będzie to O(n)skomplikowane i spójrz przynajmniej na te dwa artykuły: frontendcollisionblog.com/javascript/2015/08/15/... i coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-zamiast Wszystkiego najlepszego!
simhumileco
1
Dziękuję @simhumileco! Właśnie z tego powodu jestem tutaj (i prawdopodobnie także wielu innych). Pytanie brzmi prawdopodobnie, jak połączyć .filter i .map tylko raz.
Jack Black
21

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

images.flatMap(({src}) => src.endsWith('.json') ? [] : src);

Z MDN :

flatMapmoże być użyty jako sposób na dodawanie i usuwanie przedmiotów (modyfikowanie liczby przedmiotów) podczas mapy. Innymi słowy, pozwala na mapowanie wielu elementów do wielu elementów (przez obsługę każdego elementu wejściowego osobno), a nie zawsze jeden do jednego. W tym sensie działa jak przeciwieństwo filtru. Wystarczy zwrócić tablicę 1-elementową, aby zachować element, tablicę wieloelementową, aby dodać elementy, lub tablicę 0-elementową, aby usunąć element.

Trevor Dixon
źródło
1
Najlepsza odpowiedź bez dwóch zdań! Więcej informacji tutaj: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Dominique PERETTI
1
to jest naprawdę odpowiedź, prosta i wystarczająco silna. dowiadujemy się, że to lepsze niż filtrowanie i zmniejszanie.
bronić orca
19

TLDR: Możesz najpierw przefiltrować tablicę, a następnie wykonać mapę, ale wymagałoby to dwóch przejść w tablicy (filtr zwraca tablicę do mapy). Ponieważ ta tablica jest niewielka, jest to bardzo mały koszt wydajności. Możesz także zrobić proste zmniejszenie. Jeśli jednak chcesz sobie wyobrazić, jak można to zrobić za pomocą pojedynczego przejścia przez tablicę (lub dowolny typ danych), możesz skorzystać z pomysłu zwanego „przetwornikami”, który stał się popularny przez Richa Hickeya.

Odpowiedź:

Nie powinniśmy wymagać zwiększania łańcucha i operowania na [].map(fn1).filter(f2)...macierzach, ponieważ takie podejście tworzy tablice pośrednie w pamięci dla każdej reducingfunkcji.

Najlepsze podejście działa na rzeczywistej funkcji redukcji, więc jest tylko jedno przejście danych i żadnych dodatkowych tablic.

Funkcja redukująca jest funkcją przekazaną do reducei pobiera akumulator i wkład ze źródła i zwraca coś, co wygląda jak akumulator

// 1. create a concat reducing function that can be passed into `reduce`
const concat = (acc, input) => acc.concat([input])

// note that [1,2,3].reduce(concat, []) would return [1,2,3]

// transforming your reducing function by mapping
// 2. create a generic mapping function that can take a reducing function and return another reducing function
const mapping = (changeInput) => (reducing) => (acc, input) => reducing(acc, changeInput(input))

// 3. create your map function that operates on an input
const getSrc = (x) => x.src
const mappingSrc = mapping(getSrc)

// 4. now we can use our `mapSrc` function to transform our original function `concat` to get another reducing function
const inputSources = [{src:'one.html'}, {src:'two.txt'}, {src:'three.json'}]
inputSources.reduce(mappingSrc(concat), [])
// -> ['one.html', 'two.txt', 'three.json']

// remember this is really essentially just
// inputSources.reduce((acc, x) => acc.concat([x.src]), [])


// transforming your reducing function by filtering
// 5. create a generic filtering function that can take a reducing function and return another reducing function
const filtering = (predicate) => (reducing) => (acc, input) => (predicate(input) ? reducing(acc, input): acc)

// 6. create your filter function that operate on an input
const filterJsonAndLoad = (img) => {
  console.log(img)
  if(img.src.split('.').pop() === 'json') {
    // game.loadSprite(...);
    return false;
  } else {
    return true;
  }
}
const filteringJson = filtering(filterJsonAndLoad)

// 7. notice the type of input and output of these functions
// concat is a reducing function,
// mapSrc transforms and returns a reducing function
// filterJsonAndLoad transforms and returns a reducing function
// these functions that transform reducing functions are "transducers", termed by Rich Hickey
// source: http://clojure.com/blog/2012/05/15/anatomy-of-reducer.html
// we can pass this all into reduce! and without any intermediate arrays

const sources = inputSources.reduce(filteringJson(mappingSrc(concat)), []);
// [ 'one.html', 'two.txt' ]

// ==================================
// 8. BONUS: compose all the functions
// You can decide to create a composing function which takes an infinite number of transducers to
// operate on your reducing function to compose a computed accumulator without ever creating that
// intermediate array
const composeAll = (...args) => (x) => {
  const fns = args
  var i = fns.length
  while (i--) {
    x = fns[i].call(this, x);
  }
  return x
}

const doABunchOfStuff = composeAll(
    filtering((x) => x.src.split('.').pop() !== 'json'),
    mapping((x) => x.src),
    mapping((x) => x.toUpperCase()),
    mapping((x) => x + '!!!')
)

const sources2 = inputSources.reduce(doABunchOfStuff(concat), [])
// ['ONE.HTML!!!', 'TWO.TXT!!!']

Zasoby: post: przetworniki Rich Hickey

theptrk
źródło
17

Oto zabawne rozwiązanie:

/**
 * Filter-map. Like map, but skips undefined values.
 *
 * @param callback
 */
function fmap(callback) {
    return this.reduce((accum, ...args) => {
        let x = callback(...args);
        if(x !== undefined) {
            accum.push(x);
        }
        return accum;
    }, []);
}

Użyj z operatorem powiązania :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
mpen
źródło
1
Metoda ta ocaliła mnie od konieczności korzystania z oddzielnych map, filteri concatrozmowy.
LogicalBranch
11

Odpowiedź bez zbędnych przypadków na krawędziach:

const thingsWithoutNulls = things.reduce((acc, thing) => {
  if (thing !== null) {
    acc.push(thing);
  }
  return acc;
}, [])
corysimmony
źródło
10

Dlaczego nie skorzystać z pętli forEach?

let arr = ['a', 'b', 'c', 'd', 'e'];
let filtered = [];

arr.forEach(x => {
  if (!x.includes('b')) filtered.push(x);
});

console.log(filtered)   // filtered === ['a','c','d','e'];

Lub jeszcze prostszy filtr:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
źródło
1
Najlepsza byłaby prosta pętla dla, która filtruje i tworzy nową tablicę, ale w kontekście użycia mappozwala zachować ją tak, jak teraz. (był 4 lata temu zadałem to pytanie, gdy nie wiedziałem nic o kodowaniu)
Ismail
Całkiem słusznie, biorąc pod uwagę, że nie ma bezpośredniej drogi do powyższego z mapą i wszystkie rozwiązania wykorzystywały alternatywną metodę, o której myślałem, że zrobię chip w najprostszy sposób, jaki mogłem zrobić tak samo.
Alex
8
var sources = images.map(function (img) {
    if(img.src.split('.').pop() === "json"){ // if extension is .json
        return null; // skip
    }
    else{
        return img.src;
    }
}).filter(Boolean);

.filter(Boolean)Będzie odfiltrować żadnych wartości falsey w danej tablicy, która w danym przypadku jest null.

Lucas P.
źródło
3

Oto metoda narzędzia (zgodna z ES5), która odwzorowuje tylko wartości niepuste (ukrywa wywołanie redukcji):

function mapNonNull(arr, cb) {
    return arr.reduce(function (accumulator, value, index, arr) {
        var result = cb.call(null, value, index, arr);
        if (result != null) {
            accumulator.push(result);
        }

        return accumulator;
    }, []);
}

var result = mapNonNull(["a", "b", "c"], function (value) {
    return value === "b" ? null : value; // exclude "b"
});

console.log(result); // ["a", "c"]

DJDaveMark
źródło
1

Używam .forEachdo iteracji i wypycham wynik do resultstablicy, a następnie go używam, dzięki temu rozwiązaniu nie będę dwa razy zapętlać tablicy

SayJeyHi
źródło
1

Aby ekstrapolować komentarz Felixa Klinga , możesz użyć .filter()tego w następujący sposób:

var sources = images.map(function (img) {
  if(img.src.split('.').pop() === "json") { // if extension is .json
    return null; // skip
  } else {
    return img.src;
  }
}).filter(Boolean);

Spowoduje to usunięcie wartości falsey z tablicy zwracanej przez .map()

Możesz to jeszcze bardziej uprościć:

var sources = images.map(function (img) {
  if(img.src.split('.').pop() !== "json") { // if extension is .json
    return img.src;
  }
}).filter(Boolean);

Lub nawet jako jednowarstwowa przy użyciu funkcji strzałki, destrukcji obiektu i &&operatora:

var sources = images.map(({ src }) => src.split('.').pop() !== "json" && src).filter(Boolean);
camslice
źródło
0

Oto zaktualizowana wersja kodu dostarczonego przez @theprtk . Jest to trochę oczyszczone, aby pokazać uogólnioną wersję, mając przykład.

Uwaga: dodam to jako komentarz do jego postu, ale nie mam jeszcze wystarczającej reputacji

/**
 * @see http://clojure.com/blog/2012/05/15/anatomy-of-reducer.html
 * @description functions that transform reducing functions
 */
const transduce = {
  /** a generic map() that can take a reducing() & return another reducing() */
  map: changeInput => reducing => (acc, input) =>
    reducing(acc, changeInput(input)),
  /** a generic filter() that can take a reducing() & return */
  filter: predicate => reducing => (acc, input) =>
    predicate(input) ? reducing(acc, input) : acc,
  /**
   * a composing() that can take an infinite # transducers to operate on
   *  reducing functions to compose a computed accumulator without ever creating
   *  that intermediate array
   */
  compose: (...args) => x => {
    const fns = args;
    var i = fns.length;
    while (i--) x = fns[i].call(this, x);
    return x;
  },
};

const example = {
  data: [{ src: 'file.html' }, { src: 'file.txt' }, { src: 'file.json' }],
  /** note: `[1,2,3].reduce(concat, [])` -> `[1,2,3]` */
  concat: (acc, input) => acc.concat([input]),
  getSrc: x => x.src,
  filterJson: x => x.src.split('.').pop() !== 'json',
};

/** step 1: create a reducing() that can be passed into `reduce` */
const reduceFn = example.concat;
/** step 2: transforming your reducing function by mapping */
const mapFn = transduce.map(example.getSrc);
/** step 3: create your filter() that operates on an input */
const filterFn = transduce.filter(example.filterJson);
/** step 4: aggregate your transformations */
const composeFn = transduce.compose(
  filterFn,
  mapFn,
  transduce.map(x => x.toUpperCase() + '!'), // new mapping()
);

/**
 * Expected example output
 *  Note: each is wrapped in `example.data.reduce(x, [])`
 *  1: ['file.html', 'file.txt', 'file.json']
 *  2:  ['file.html', 'file.txt']
 *  3: ['FILE.HTML!', 'FILE.TXT!']
 */
const exampleFns = {
  transducers: [
    mapFn(reduceFn),
    filterFn(mapFn(reduceFn)),
    composeFn(reduceFn),
  ],
  raw: [
    (acc, x) => acc.concat([x.src]),
    (acc, x) => acc.concat(x.src.split('.').pop() !== 'json' ? [x.src] : []),
    (acc, x) => acc.concat(x.src.split('.').pop() !== 'json' ? [x.src.toUpperCase() + '!'] : []),
  ],
};
const execExample = (currentValue, index) =>
  console.log('Example ' + index, example.data.reduce(currentValue, []));

exampleFns.raw.forEach(execExample);
exampleFns.transducers.forEach(execExample);
Sid
źródło
0

Możesz użyć metody po tobie map(). Metoda filter()na przykład w twoim przypadku:

var sources = images.map(function (img) {
  if(img.src.split('.').pop() === "json"){ // if extension is .json
    return null; // skip
  }
  else {
    return img.src;
  }
});

Filtr metod:

const sourceFiltered = sources.filter(item => item)

Następnie tylko istniejące elementy znajdują się w nowej tablicy sourceFiltered.

Cristhian D.
źródło