Come saltare un elemento in .map ()?


418

Come posso saltare un elemento array in .map?

Il mio codice:

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

Questo restituirà:

["img.png", null, "img.png"]

19
Non puoi, ma puoi filtrare tutti i valori null in seguito.
Felix Kling,

1
Perchè no? So che usare continue non funziona ma sarebbe bene sapere perché (anche evitare il doppio loop) - modifica - per il tuo caso non potresti semplicemente invertire la condizione if e tornare solo img.srcse il risultato del pop split! = = json?
GrayedFox

@GrayedFox Quindi implicito undefinedverrebbe inserito nell'array, anziché null. Non così meglio ...
FZ

Risposte:


640

Solo .filter()prima:

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

Se non vuoi farlo, il che non è irragionevole dal momento che ha dei costi, puoi usare il più generale .reduce(). In genere puoi esprimere .map()in termini di .reduce:

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

può essere scritto come

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

Quindi, se devi saltare elementi, puoi farlo facilmente con .reduce():

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

In quella versione, il codice nel .filter()primo esempio fa parte del .reduce()callback. La sorgente dell'immagine viene inserita nell'array dei risultati solo nel caso in cui l'operazione di filtro l'avrebbe mantenuta.


21
Non è necessario eseguire il loop sull'intero array due volte? C'è un modo per evitarlo?
Alex McMillan,

7
@AlexMcMillan potresti usare .reduce()e fare tutto in una volta, anche se per quanto riguarda le prestazioni dubito che farebbe una differenza significativa.
Punta il

9
Con tutti questi negativo, "vuoto" valori -style ( null, undefined, NaNecc) sarebbe bene se si potesse utilizzare uno dentro una map()come indicatore che questo oggetto associa a nulla e dovrebbe essere saltato. Mi capita spesso di trovare array di cui desidero mappare il 98% (ad es. String.split()Lasciando una singola stringa vuota alla fine, di cui non mi interessa). Grazie per la risposta :)
Alex McMillan,

6
@AlexMcMillan .reduce()è una sorta di funzione "fai quello che vuoi", perché hai il controllo completo sul valore di ritorno. Potresti essere interessato all'eccellente lavoro di Rich Hickey in Clojure riguardante il concetto di trasduttori .
Punta il

3
@vsync con cui non puoi saltare un elemento .map(). Puoi invece usare .reduce()invece, quindi lo aggiungerò.
Punta il

25

Penso che il modo più semplice per saltare alcuni elementi da un array sia usando il metodo filter () .

Usando questo metodo ( ES5 ) e la sintassi ES6 è possibile scrivere il codice in una riga e questo restituirà ciò che si desidera :

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


1
questo è esattamente ciò per cui è .filter()stato creato
valanga1 il

2
È meglio di forEache completarlo in un passaggio invece di due?
wuliwong,

1
Come desideri @wuliwong. Ma tieni presente che questo sarà ancora O(n)in misura di complessità e guarda almeno anche questi due articoli: frontendcollisionblog.com/javascript/2015/08/15/… e coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-invece Tutto il meglio!
simhumileco,

1
Grazie @simhumileco! Proprio per questo, sono qui (e probabilmente anche molti altri). La domanda è probabilmente come combinare .filter e .map ripetendo una sola volta.
Jack Black il

21

Dal 2019, Array.prototype.flatMap è una buona opzione.

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

Da MDN :

flatMappuò essere usato come un modo per aggiungere e rimuovere elementi (modificare il numero di oggetti) durante una mappa. In altre parole, consente di mappare molti elementi su molti elementi (gestendo ciascun elemento di input separatamente), anziché sempre uno a uno. In questo senso, funziona come l'opposto del filtro. Restituisci semplicemente un array a 1 elemento per mantenere l'elemento, un array a più elementi per aggiungere elementi o un array a 0 elementi per rimuovere l'elemento.


1
La migliore risposta senza dubbio! Maggiori informazioni qui: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Dominique PERETTI,

1
questa è la risposta davvero, semplice e abbastanza forte. apprendiamo che è meglio che filtrare e ridurre.
difendi orca il

19

TLDR: è possibile prima filtrare l'array e quindi eseguire la mappa, ma ciò richiederebbe due passaggi sull'array (il filtro restituisce un array alla mappa). Poiché questo array è piccolo, ha un costo di prestazione molto piccolo. Puoi anche fare una semplice riduzione. Tuttavia, se si desidera reinventare il modo in cui ciò può essere fatto con un singolo passaggio sull'array (o qualsiasi tipo di dati), è possibile utilizzare un'idea chiamata "trasduttori" resa popolare da Rich Hickey.

Risposta:

Non dovremmo richiedere un aumento del concatenamento dei punti e il funzionamento sull'array [].map(fn1).filter(f2)...poiché questo approccio crea array intermedi in memoria su ogni reducingfunzione.

L'approccio migliore opera sull'effettiva funzione di riduzione, pertanto esiste un solo passaggio di dati e nessun array aggiuntivo.

La funzione di riduzione è la funzione passata in reducee prende un accumulatore e input dalla sorgente e restituisce qualcosa che assomiglia all'accumulatore

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

Risorse: post ricco di trasduttori hickey


17

Ecco una soluzione divertente:

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

Utilizzare con l' operatore di bind :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]

1
Questo metodo mi ha salvato da dover utilizzare separati map, filtere concatle chiamate.
LogicalBranch,

11

Risposta senza casi superflui:

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

10

Perché non usare semplicemente un ciclo 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'];

O ancora più semplice utilizzare il filtro:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];

1
La cosa migliore sarebbe un semplice ciclo per filtrare e creare un nuovo array, ma per il contesto dell'utilizzo mapmanteniamolo com'è adesso. (4 anni fa ho fatto questa domanda, quando non sapevo nulla di programmazione)
Ismail,

Abbastanza giusto, dato che non esiste un modo diretto per quanto sopra con map e tutte le soluzioni hanno usato un metodo alternativo che ho pensato di chip nel modo più semplice in cui potevo pensare di fare lo stesso.
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);

Il .filter(Boolean)filtrerà i valori Falsey in un dato array, che nel caso è la null.


3

Ecco un metodo di utilità (compatibile ES5) che mappa solo valori non nulli (nasconde la chiamata per ridurre):

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"]


1

Uso .forEachper iterare e spingere il risultato resultssull'array, quindi lo uso, con questa soluzione non eseguirò il loop sull'array due volte


1

Per estrapolare il commento di Felix Kling , puoi usare in .filter()questo modo:

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

Ciò rimuoverà i valori di falso dall'array che viene restituito da .map()

Potresti semplificarlo ulteriormente in questo modo:

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

O anche come one-liner usando una funzione freccia, la distruzione degli oggetti e l' &&operatore:

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

0

Ecco una versione aggiornata del codice fornito da @theprtk . È un po 'pulito per mostrare la versione generalizzata pur avendo un esempio.

Nota: lo aggiungerei come commento al suo post ma non ho ancora abbastanza reputazione

/**
 * @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);

0

Puoi usare il tuo metodo dopo di te map(). Il metodo filter()ad esempio nel tuo caso:

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

Il filtro del metodo:

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

Quindi, solo gli elementi esistenti sono nel nuovo array sourceFiltered.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.