Comment ignorer un élément dans .map ()?

418

Comment puis-je ignorer un élément de tableau .map?

Mon code:

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

Cela reviendra:

["img.png", null, "img.png"]
Ismail
la source
19
Vous ne pouvez pas, mais vous pouvez ensuite filtrer toutes les valeurs nulles.
Felix Kling
1
Pourquoi pas? Je sais que l'utilisation de continue ne fonctionne pas, mais il serait bon de savoir pourquoi (cela éviterait également la double boucle) - éditer - pour votre cas, vous ne pourriez pas simplement inverser la condition if et ne retourner que img.srcsi le résultat de la division pop! = = json?
GrayedFox
@GrayedFox Alors implicite undefinedserait mis dans le tableau, au lieu de null. Pas mieux ...
FZ le

Réponses:

640

Tout d' .filter()abord:

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

Si vous ne voulez pas faire cela, ce qui n'est pas déraisonnable car cela a un coût, vous pouvez utiliser le plus général .reduce(). Vous pouvez généralement exprimer .map()en termes de .reduce:

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

peut être écrit comme

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

Donc, si vous devez ignorer des éléments, vous pouvez le faire facilement avec .reduce():

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

Dans cette version, le code .filter()du premier exemple fait partie du .reduce()rappel. La source d'image n'est poussée sur le tableau de résultats que dans le cas où l'opération de filtrage l'aurait conservée.

Pointu
la source
21
Cela ne nécessite-t-il pas que vous boucliez deux fois l'ensemble du tableau? Y a-t-il un moyen d'éviter cela?
Alex McMillan
7
@AlexMcMillan, vous pouvez utiliser .reduce()et tout faire en une seule fois, mais en termes de performances, je doute que cela ferait une différence significative.
Pointy
9
Avec tous ces négatifs, « vide » valeurs -style ( null, undefined, NaNetc.) , il serait bien si nous pouvions utiliser un à l' intérieur d' un map()comme un indicateur que cet objet mappé à rien et doit être ignoré. Je rencontre souvent des tableaux que je veux mapper à 98% (par exemple: String.split()laisser une seule chaîne vide à la fin, ce qui ne m'importe pas). Merci pour votre réponse :)
Alex McMillan
6
@AlexMcMillan .reduce()est bien une sorte de fonction de base "faites ce que vous voulez", car vous avez un contrôle complet sur la valeur de retour. Vous pourriez être intéressé par l'excellent travail de Rich Hickey à Clojure concernant le concept de transducteurs .
Pointy
3
@vsync, vous ne pouvez pas ignorer un élément avec .map(). Vous pouvez cependant utiliser à la .reduce()place, donc je vais ajouter cela.
Pointy
25

Je pense que la façon la plus simple de sauter certains éléments d'un tableau est d'utiliser la méthode filter () .

En utilisant cette méthode ( ES5 ) et la syntaxe ES6, vous pouvez écrire votre code sur une seule ligne , et cela retournera ce que vous voulez :

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
la source
1
c'est exactement ce .filter()pour quoi
avalanche1
2
Est-ce mieux que de le forEachterminer en un seul passage au lieu de deux?
wuliwong
1
Comme vous le souhaitez @wuliwong. Mais veuillez prendre en compte que ce sera toujours O(n)dans la mesure de la complexité et veuillez également consulter au moins ces deux articles: frontendcollisionblog.com/javascript/2015/08/15/… et coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-place Meilleurs vœux!
simhumileco
1
Merci @simhumileco! Exactement à cause de cela, je suis ici (et probablement beaucoup d'autres aussi). La question est probablement de savoir comment combiner .filter et .map en répétant une seule fois.
Jack Black
21

Depuis 2019, Array.prototype.flatMap est une bonne option.

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

De MDN :

flatMappeut être utilisé comme un moyen d'ajouter et de supprimer des éléments (modifier le nombre d'éléments) pendant une carte. En d'autres termes, il vous permet de mapper de nombreux éléments à de nombreux éléments (en traitant chaque élément d'entrée séparément), plutôt que toujours un à un. En ce sens, cela fonctionne comme l'opposé du filtre. Renvoyez simplement un tableau à 1 élément pour conserver l'élément, un tableau à plusieurs éléments pour ajouter des éléments ou un tableau à 0 élément pour supprimer l'élément.

Trevor Dixon
la source
1
Meilleure réponse haut la main! Plus d'informations ici: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Dominique PERETTI
1
c'est vraiment la réponse, simple et assez forte. nous apprenons que c'est mieux que filtrer et réduire.
défendre orque
19

TLDR: Vous pouvez d'abord filtrer votre tableau, puis effectuer votre carte, mais cela nécessiterait deux passes sur le tableau (le filtre renvoie un tableau sur la carte). Étant donné que ce tableau est petit, il s'agit d'un coût de performance très faible. Vous pouvez également effectuer une simple réduction. Cependant, si vous voulez réimaginer comment cela peut être fait avec un seul passage sur le tableau (ou tout type de données), vous pouvez utiliser une idée appelée "transducteurs" rendue populaire par Rich Hickey.

Réponse:

Nous ne devrions pas exiger d'augmenter le chaînage de points et d'opérer sur le tableau, [].map(fn1).filter(f2)...car cette approche crée des tableaux intermédiaires en mémoire sur chaque reducingfonction.

La meilleure approche fonctionne sur la fonction de réduction réelle, il n'y a donc qu'un seul passage de données et aucun tableau supplémentaire.

La fonction de réduction est la fonction transmise reduceet prend un accumulateur et une entrée de la source et renvoie quelque chose qui ressemble à l'accumulateur

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

Ressources: publication de transducteurs riches en suçon

theptrk
la source
17

Voici une solution amusante:

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

À utiliser avec l' opérateur de liaison :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
mpen
la source
1
Cette méthode m'a sauvé d'avoir à utiliser séparés map, filteret les concatappels.
LogicalBranch
11

Répondre aux cas de bord sans superflu:

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

Pourquoi ne pas simplement utiliser une boucle 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'];

Ou filtre d'utilisation encore plus simple:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
la source
1
Le mieux serait une boucle for simple qui filtre et crée un nouveau tableau, mais pour le contexte d'utilisation, maplaissez-le tel qu'il est maintenant. (il y a 4 ans j'ai posé cette question, alors que je ne connaissais rien au codage)
Ismail
Assez juste, étant donné qu'il n'y a pas de chemin direct vers ce qui précède avec la carte et que toutes les solutions ont utilisé une méthode alternative, j'ai pensé que je cracherais de la manière la plus simple à laquelle j'aurais pu penser pour faire de même.
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);

Le .filter(Boolean)filtrera toutes les valeurs de falsey dans un tableau donné, qui dans votre cas est le null.

Lucas P.
la source
3

Voici une méthode utilitaire (compatible ES5) qui mappe uniquement les valeurs non nulles (masque l'appel à réduire):

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
la source
1

J'utilise .forEachpour itérer et pousser le résultat vers le resultstableau puis l'utiliser, avec cette solution, je ne boucle pas deux fois sur le tableau

SayJeyHi
la source
1

Pour extrapoler sur le commentaire de Felix Kling , vous pouvez utiliser .filter()comme ceci:

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

Cela supprimera les valeurs de falsey du tableau renvoyé par .map()

Vous pouvez le simplifier davantage comme ceci:

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

Ou même en une ligne à l'aide d'une fonction de flèche, de la déstructuration d'objets et de l' &&opérateur:

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

Voici une version mise à jour du code fourni par @theprtk . C'est un peu nettoyé pour montrer la version généralisée tout en ayant un exemple.

Remarque: je voudrais ajouter ceci en tant que commentaire à son article, mais je n'ai pas encore assez de réputation

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

Vous pouvez utiliser après votre méthode map(). La méthode filter()par exemple dans votre cas:

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

Le filtre de méthode:

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

Ensuite, seuls les éléments existants sont dans le nouveau tableau sourceFiltered.

Cristhian D
la source