¿Cómo omitir un elemento en .map ()?

418

¿Cómo puedo omitir un elemento de matriz .map?

Mi código:

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

Esto devolverá:

["img.png", null, "img.png"]
Ismail
fuente
19
No puede, pero puede filtrar todos los valores nulos después.
Felix Kling
1
Por qué no? Sé que usar continuar no funciona, pero sería bueno saber por qué (también evitaría el doble bucle) - editar - para su caso, ¿no podría simplemente invertir la condición if y solo regresar img.srcsi el resultado de la división pop! = = json?
GrayedFox
@GrayedFox Entonces implícito undefinedse pondría en la matriz, en lugar de null. No es mejor ...
FZs

Respuestas:

640

Solo .filter()es lo primero:

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

Si no desea hacer eso, lo cual no es irrazonable ya que tiene algún costo, puede usar el más general .reduce(). Generalmente puede expresarse .map()en términos de .reduce:

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

Se puede escribir como

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

Entonces, si necesita omitir elementos, puede hacerlo fácilmente con .reduce():

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

En esa versión, el código .filter()del primer ejemplo es parte de la .reduce()devolución de llamada. La fuente de la imagen solo se inserta en la matriz de resultados en el caso en que la operación de filtro la hubiera mantenido.

Puntiagudo
fuente
21
¿No requiere esto que repitas la matriz completa dos veces? ¿Hay alguna manera de evitar eso?
Alex McMillan
77
@AlexMcMillan podría usarlo .reduce()y hacerlo todo de una sola vez, aunque en cuanto al rendimiento, dudo que haga una diferencia significativa.
Puntiagudo
99
Con todos estos negativos, "vaciar" valores al estilo de ( null, undefined, NaNetc) sería bueno si pudiéramos utilizar uno dentro de una map()como un indicador de que este objeto se asigna a nada y se debe omitir. A menudo me encuentro con matrices de las que quiero mapear el 98% (por ejemplo, String.split()dejando una sola cadena vacía al final, lo que no me importa). Gracias por tu respuesta :)
Alex McMillan
66
@AlexMcMillan .reduce()es una especie de función básica de "haz lo que quieras", porque tienes control total sobre el valor de retorno. Quizás le interese el excelente trabajo de Rich Hickey en Clojure sobre el concepto de transductores .
Puntiagudo
3
@vsync con el que no puede omitir un elemento .map(). Sin embargo, puede usar .reduce()en su lugar, así que lo agregaré.
Puntiagudo
25

Creo que la forma más sencilla de omitir algunos elementos de una matriz es mediante el método filter () .

Al usar este método ( ES5 ) y la sintaxis de ES6 , puede escribir su código en una línea , y esto le devolverá lo que desea :

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
fuente
1
eso es exactamente para lo que .filter()fue hecho
avalancha1
2
¿Es esto mejor forEachy completarlo en una pasada en lugar de dos?
wuliwong
1
Como quieras @wuliwong. Pero tenga en cuenta que esto seguirá siendo una O(n)medida de complejidad y, al menos, mire también estos dos artículos: frontendcollisionblog.com/javascript/2015/08/15/… y coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-lugar ¡ Todo lo mejor!
simhumileco
1
Gracias @simhumileco! Exactamente por eso, estoy aquí (y probablemente muchos otros también). La pregunta es probablemente cómo combinar .filter y .map solo iterando una vez.
Jack Black
21

Desde 2019, Array.prototype.flatMap es una buena opción.

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

De MDN :

flatMapse puede usar como una forma de agregar y eliminar elementos (modificar el número de elementos) durante un mapa. En otras palabras, le permite asignar muchos elementos a muchos elementos (manejando cada elemento de entrada por separado), en lugar de siempre uno a uno. En este sentido, funciona como lo opuesto al filtro. Simplemente devuelva una matriz de 1 elemento para mantener el elemento, una matriz de elementos múltiples para agregar elementos o una matriz de 0 elementos para eliminar el elemento.

Trevor Dixon
fuente
1
¡La mejor respuesta sin dudas! Más información aquí: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Dominique PERETTI
1
Esta es realmente la respuesta, simple y lo suficientemente fuerte. aprendemos que esto es mejor que filtrar y reducir.
defender orca
19

TLDR: primero puede filtrar su matriz y luego realizar su mapa, pero esto requeriría dos pases en la matriz (el filtro devuelve una matriz al mapa). Como esta matriz es pequeña, es un costo de rendimiento muy pequeño. También puedes hacer una simple reducción. Sin embargo, si desea volver a imaginar cómo se puede hacer esto con un solo paso sobre la matriz (o cualquier tipo de datos), puede usar una idea llamada "transductores" popularizada por Rich Hickey.

Responder:

No deberíamos requerir aumentar el encadenamiento de puntos y operar en la matriz [].map(fn1).filter(f2)...ya que este enfoque crea matrices intermedias en la memoria en cada reducingfunción.

El mejor enfoque opera en la función de reducción real, por lo que solo hay un pase de datos y no hay matrices adicionales.

La función reductora es la función que se pasa reducey toma un acumulador y la entrada de la fuente y devuelve algo que se parece al acumulador

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

Recursos: ricos transductores de chupetón post

theptrk
fuente
17

Aquí hay una solución divertida:

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

Usar con el operador de enlace :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
mpen
fuente
1
Este método me salvó de tener que utilizar por separado map, filtery concatlas llamadas.
LogicalBranch
11

Responda sin casos extremos superfluos:

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

¿Por qué no solo usar un bucle 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 incluso un filtro de uso más simple:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
fuente
1
Lo mejor sería un ciclo simple que filtre y cree una nueva matriz, pero para el contexto de uso, mapvamos a mantenerlo como está ahora. (hace 4 años hice esta pregunta, cuando no sabía nada sobre codificación)
Ismail
Es justo, dado que no hay una forma directa de lo anterior con el mapa y todas las soluciones usaron un método alternativo que pensé que haría con el chip de la manera más simple que pudiera pensar para hacer lo mismo.
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);

El .filter(Boolean)filtrará cualquier valor Falsey en una matriz dada, que en su caso es el null.

Lucas P.
fuente
3

Aquí hay un método de utilidad (compatible con ES5) que solo asigna valores no nulos (oculta la llamada a reducir):

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

Utilizo .forEachpara iterar, y empujo el resultado a la resultsmatriz y luego lo uso, con esta solución no recorreré la matriz dos veces

SayJeyHi
fuente
1

Para extrapolar el comentario de Felix Kling , puede usar .filter()así:

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

Eso eliminará los valores de falsey de la matriz que devuelve .map()

Podrías simplificarlo aún más así:

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

O incluso como una línea utilizando una función de flecha, la desestructuración de objetos y el &&operador:

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

Aquí hay una versión actualizada del código provisto por @theprtk . Se limpia un poco para mostrar la versión generalizada mientras se tiene un ejemplo.

Nota: agregaría esto como un comentario a su publicación, pero todavía no tengo suficiente reputación

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

Puede usar después de su método map(). El método, filter()por ejemplo, en su caso:

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

El filtro del método:

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

Entonces, solo los elementos existentes están en la nueva matriz sourceFiltered.

Cristhian D
fuente