Wie überspringe ich ein Element in .map ()?

413

Wie kann ich ein Array-Element überspringen .map?

Mein Code:

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

Dies wird zurückkehren:

["img.png", null, "img.png"]
Ismail
quelle
18
Sie können nicht, aber Sie können anschließend alle Nullwerte herausfiltern.
Felix Kling
1
Warum nicht? Ich weiß, dass die Verwendung von continue nicht funktioniert, aber es wäre gut zu wissen, warum (würde auch Doppelschleifen vermeiden) - edit - für Ihren Fall könnten Sie nicht einfach die if-Bedingung invertieren und nur zurückkehren, img.srcwenn das Ergebnis des Split-Pop! = = json?
GrayedFox
@GrayedFox Dann wird implizit undefinedanstelle von in das Array eingefügt null. Nicht so besser ...
FZs

Antworten:

636

Nur .filter()zuerst:

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

Wenn Sie dies nicht tun möchten, was nicht unangemessen ist, da es einige Kosten verursacht, können Sie das allgemeinere verwenden .reduce(). Sie können allgemein ausdrücken .map()in Bezug auf .reduce:

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

kann geschrieben werden als

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

Wenn Sie also Elemente überspringen müssen, können Sie dies ganz einfach tun mit .reduce():

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

In dieser Version ist der Code .filter()aus dem ersten Beispiel Teil des .reduce()Rückrufs. Die Bildquelle wird nur dann auf das Ergebnisarray verschoben, wenn die Filteroperation sie beibehalten hätte.

Spitze
quelle
21
Erfordert dies nicht, dass Sie das gesamte Array zweimal durchlaufen? Gibt es eine Möglichkeit, dies zu vermeiden?
Alex McMillan
7
@AlexMcMillan Sie könnten .reduce()alles in einem Durchgang verwenden, obwohl ich in Bezug auf die Leistung bezweifle, dass dies einen signifikanten Unterschied bewirken würde.
Pointy
9
Mit all diesen negativen „leeren“ -Stil Werten ( null, undefined, NaNusw.) , es wäre gut , wenn wir eine innerhalb eines verwenden könnten map()als Indikator , dass dieses Objekt zu nichts abbildet und übersprungen werden soll. Ich stoße oft auf Arrays, von denen ich 98% zuordnen möchte (z. B. String.split()am Ende eine einzelne, leere Zeichenfolge belassen, die mir egal ist). Vielen Dank für Ihre Antwort :)
Alex McMillan
6
@AlexMcMillan .reduce()ist eine Art Baseline-Funktion "Mach was immer du willst", da du die vollständige Kontrolle über den Rückgabewert hast. Vielleicht interessieren Sie auch die hervorragende Arbeit von Rich Hickey in Clojure zum Konzept der Wandler .
Pointy
3
@vsync Sie können kein Element mit überspringen .map(). Sie können jedoch .reduce()stattdessen verwenden, also werde ich das hinzufügen.
Pointy
25

Ich denke, der einfachste Weg, einige Elemente aus einem Array zu überspringen, ist die Verwendung der filter () -Methode.

Mit dieser Methode ( ES5 ) und der ES6- Syntax können Sie Ihren Code in eine Zeile schreiben , und dies gibt zurück, was Sie wollen :

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
quelle
1
das ist genau das, was .filter()für gemacht wurde
avalanche1
2
Ist das besser als forEachund in einem Durchgang statt in zwei?
Wuliwong
1
Wie du willst @wuliwong. O(n)Beachten Sie jedoch, dass dies immer noch komplex sein wird, und schauen Sie sich zumindest auch diese beiden Artikel an: frontendcollisionblog.com/javascript/2015/08/15/… und coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-anstatt Alles Gute!
Simhumileco
1
Vielen Dank, dass Sie @simhumileco! Genau deshalb bin ich hier (und wahrscheinlich auch viele andere). Die Frage ist wahrscheinlich, wie man .filter und .map kombiniert, indem man nur einmal iteriert.
Jack Black
20

Seit 2019 ist Array.prototype.flatMap eine gute Option.

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

Von MDN :

flatMapkann verwendet werden, um Elemente während einer Karte hinzuzufügen und zu entfernen (die Anzahl der Elemente ändern). Mit anderen Worten, Sie können viele Elemente vielen Elementen zuordnen (indem Sie jedes Eingabeelement separat behandeln) und nicht immer eins zu eins. In diesem Sinne funktioniert es wie das Gegenteil von Filter. Geben Sie einfach ein Array mit 1 Elementen zurück, um das Element beizubehalten, ein Array mit mehreren Elementen, um Elemente hinzuzufügen, oder ein Array mit 0 Elementen, um das Element zu entfernen.

Trevor Dixon
quelle
1
Beste Antwort zweifellos! Weitere Infos hier: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Dominique PERETTI
1
Das ist die wirkliche Antwort, einfach und stark genug. Wir lernen, dass dies besser ist als filtern und reduzieren.
Orca
19

TLDR: Sie können zuerst Ihr Array filtern und dann Ihre Zuordnung durchführen, dies würde jedoch zwei Durchgänge für das Array erfordern (Filter gibt ein Array an die Zuordnung zurück). Da dieses Array klein ist, sind die Leistungskosten sehr gering. Sie können auch eine einfache Reduzierung durchführen. Wenn Sie sich jedoch noch einmal vorstellen möchten, wie dies mit einem einzigen Durchgang über das Array (oder einen beliebigen Datentyp) erreicht werden kann, können Sie eine Idee namens "Transducer" verwenden, die von Rich Hickey populär gemacht wurde.

Antworten:

Es sollte nicht erforderlich sein, die Punktverkettung zu erhöhen und das Array zu bearbeiten, [].map(fn1).filter(f2)...da bei diesem Ansatz für jede reducingFunktion Zwischenarrays im Speicher erstellt werden .

Der beste Ansatz arbeitet mit der eigentlichen Reduktionsfunktion, sodass nur ein Datendurchlauf und keine zusätzlichen Arrays vorhanden sind.

Die Reduktionsfunktion ist die Funktion, die an reduceeinen Akkumulator und eine Eingabe von der Quelle übergeben wird und etwas zurückgibt, das dem Akkumulator ähnelt

// 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!!!']

Ressourcen: Rich Hickey Transducer Post

theptrk
quelle
16

Hier ist eine unterhaltsame Lösung:

/**
 * 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;
    }, []);
}

Verwendung mit dem Bind-Operator :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
mpen
quelle
1
Diese Methode hat mich gerettet aus mit eigenen verwenden map, filterund concatAnrufe.
LogicalBranch
11

Antwort ohne überflüssige Randfälle:

const thingsWithoutNulls = things.reduce((acc, thing) => {
  if (thing !== null) {
    acc.push(thing);
  }
  return acc;
}, [])
Corysimmons
quelle
10

Warum nicht einfach eine forEach-Schleife verwenden?

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'];

Oder noch einfacher Filter verwenden:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
quelle
1
Am besten wäre eine einfache for-Schleife, die ein neues Array filtert und erstellt, aber für den Kontext der Verwendung maplassen Sie es so, wie es jetzt ist. (war vor 4 Jahren Ich stellte diese Frage, als ich nichts über Codierung wusste)
Ismail
Fair genug, da es mit map keinen direkten Weg zum oben genannten gibt und alle Lösungen eine alternative Methode verwendeten, von der ich dachte, ich würde auf die einfachste Art und Weise chippen, die ich mir vorstellen kann, um dasselbe zu tun.
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);

Das .filter(Boolean)filtert alle Falsey-Werte in einem bestimmten Array heraus, in Ihrem Fall das null.

Lucas P.
quelle
3

Hier ist eine Dienstprogrammmethode (ES5-kompatibel), die nur Nicht-Null-Werte zuordnet (verbirgt den Aufruf zum Reduzieren):

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
quelle
1

Ich verwende es .forEach, um zu iterieren und das Ergebnis auf das resultsArray zu übertragen, und verwende es dann. Mit dieser Lösung werde ich das Array nicht zweimal durchlaufen

SayJeyHi
quelle
1

Um den Kommentar von Felix Kling zu extrapolieren , können Sie Folgendes verwenden .filter():

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

Dadurch werden Falsey-Werte aus dem Array entfernt, das von zurückgegeben wird .map()

Sie könnten es so weiter vereinfachen:

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

Oder sogar als Einzeiler mit Pfeilfunktion, Objektzerstörung und &&Bediener:

var sources = images.map(({ src }) => src.split('.').pop() !== "json" && src).filter(Boolean);
Camslice
quelle
0

Hier ist eine aktualisierte Version des von @theprtk bereitgestellten Codes . Es ist ein wenig aufgeräumt, um die verallgemeinerte Version zu zeigen, während man ein Beispiel hat.

Hinweis: Ich würde dies als Kommentar zu seinem Beitrag hinzufügen, aber ich habe noch nicht genug Ruf

/**
 * @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
quelle
0

Sie können nach Ihrer Methode verwenden map(). Die Methode filter()zum Beispiel in Ihrem Fall:

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

Der Methodenfilter:

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

Dann befinden sich nur die vorhandenen Elemente im neuen Array sourceFiltered.

Cristhian D.
quelle