Como pular um elemento em .map ()?

418

Como posso pular um elemento de matriz .map?

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

Isso retornará:

["img.png", null, "img.png"]
Ismail
fonte
19
Você não pode, mas pode filtrar todos os valores nulos posteriormente.
Felix Kling
1
Por que não? Eu sei que o uso de continue não funciona, mas seria bom saber por que (também evitaria o loop duplo) - editar - para o seu caso, você não poderia simplesmente inverter a condição if e retornar apenas img.srcse o resultado da divisão pop! = = json?
precisa saber é o seguinte
@GrayedFox Então implícito undefinedseria colocado na matriz, em vez de null. Não que melhor ...
FZs 07/04

Respostas:

640

Apenas .filter()primeiro:

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

Se você não quiser fazer isso, o que não é irracional, pois tem algum custo, você pode usar o mais geral .reduce(). Você geralmente pode expressar .map()em termos de .reduce:

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

pode ser escrito como

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

Portanto, se você precisar pular elementos, poderá fazer isso facilmente com .reduce():

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

Nessa versão, o código .filter()do primeiro exemplo faz parte do .reduce()retorno de chamada. A fonte da imagem é enviada apenas para a matriz de resultados no caso em que a operação de filtro a teria mantido.

Pontudo
fonte
21
Isso não requer que você faça loop sobre toda a matriz duas vezes? Existe alguma maneira de evitar isso?
Alex McMillan
7
@AlexMcMillan você pode usar .reduce()e fazer tudo de uma só vez, embora em termos de desempenho duvide que isso faça uma diferença significativa.
Pointy
9
Com todos estes negativos, "esvaziar" valores -estilo ( null, undefined, NaNetc.) que seria bom se pudéssemos utilizar um dentro de um map()como um indicador de que este objeto mapeia para nada e deve ser ignorada. Costumo encontrar matrizes que quero mapear 98% de (por exemplo: String.split()deixar uma única string vazia no final, com a qual não me importo). Obrigado pela sua resposta :)
Alex McMillan
6
O @AlexMcMillan também .reduce()é uma função da linha de base "faça o que você quiser", porque você tem controle total sobre o valor de retorno. Você pode estar interessado no excelente trabalho de Rich Hickey em Clojure sobre o conceito de transdutores .
Pointy
3
@vsync você não pode pular um elemento .map(). No entanto, você pode usar .reduce(), então adicionarei isso.
Pointy
25

Eu acho que a maneira mais simples de pular alguns elementos de uma matriz é usando o método filter () .

Usando esse método ( ES5 ) e a sintaxe ES6, você pode escrever seu código em uma linha e isso retornará o que você deseja :

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
fonte
1
Isso é exatamente o que .filter()foi feito para
avalanche1
2
Isso é melhor do que forEachcompletá-lo em uma passagem em vez de duas?
Wuliwong 01/10/19
1
Como você deseja @wuliwong. Mas lembre-se de que isso ainda será uma medida de O(n)complexidade e consulte pelo menos esses dois artigos também: frontendcollisionblog.com/javascript/2015/08/15/… e coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-invés Tudo de bom!
Simhumileco 02/10/19
1
Obrigado @simhumileco! Exatamente por isso, estou aqui (e provavelmente muitos outros também). A questão é provavelmente como combinar .filter e .map iterando apenas uma vez.
Jack Black
21

Desde 2019, o Array.prototype.flatMap é uma boa opção.

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

Do MDN :

flatMappode ser usado como uma maneira de adicionar e remover itens (modificar o número de itens) durante um mapa. Em outras palavras, ele permite mapear muitos itens para muitos itens (manipulando cada item de entrada separadamente), em vez de sempre um a um. Nesse sentido, funciona como o oposto do filtro. Simplesmente retorne uma matriz de 1 elemento para manter o item, uma matriz de vários elementos para adicionar itens ou uma matriz de 0 elementos para remover o item.

Trevor Dixon
fonte
1
Melhor resposta mãos para baixo! Mais informações aqui: developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/…
Dominique PERETTI
1
Esta é realmente a resposta, simples e forte o suficiente. aprendemos que isso é melhor do que filtrar e reduzir.
defender orca
19

TLDR: você pode primeiro filtrar sua matriz e depois executar seu mapa, mas isso exigiria duas passagens na matriz (o filtro retorna uma matriz para mapear). Como essa matriz é pequena, é um custo de desempenho muito pequeno. Você também pode fazer uma simples redução. No entanto, se você quiser repensar como isso pode ser feito com uma única passagem pela matriz (ou qualquer tipo de dados), você pode usar uma idéia chamada "transdutores" popularizada por Rich Hickey.

Responda:

Não devemos exigir o aumento do encadeamento de pontos e a operação na matriz, [].map(fn1).filter(f2)...pois essa abordagem cria matrizes intermediárias na memória em todas as reducingfunções.

A melhor abordagem opera na função de redução real, para que haja apenas uma passagem de dados e nenhuma matriz extra.

A função redutora é a função passada reducee pega um acumulador e entrada da fonte e retorna algo que se parece com o 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: pós-transdutores ricos de hickey

theptrk
fonte
17

Aqui está uma solução 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;
    }, []);
}

Use com o operador de ligação :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
mpen
fonte
1
Este método me salvou de ter que usar separadas map, filtere concatchamadas.
LogicalBranch 20/05/19
11

Responda sem casos extremos supérfluos:

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

Por que não usar apenas um loop 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 filtro de uso ainda mais simples:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
fonte
1
O melhor seria um loop for simples que filtra e cria uma nova matriz, mas, para o contexto de uso, mapvamos mantê-lo como está agora. (era 4yrs atrás eu fiz esta pergunta, quando eu não sabia nada sobre codificação)
Ismail
É justo, já que não existe um caminho direto para o exposto acima, e todas as soluções usavam um método alternativo que eu pensava em usar da maneira mais simples possível.
27418 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);

O .filter(Boolean)filtro filtrará quaisquer valores de falsey em um determinado array, que no seu caso é o null.

Lucas P.
fonte
3

Aqui está um método utilitário (compatível com ES5) que mapeia apenas valores não nulos (oculta a chamada para redução):

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

Eu uso .forEachpara iterar, e empurro o resultado para o resultsarray e depois o uso. Com esta solução, não repetirei o array duas vezes

SayJeyHi
fonte
1

Para extrapolar no comentário de Felix Kling , você pode usar .filter()assim:

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

Isso removerá os valores falsey da matriz retornada por .map()

Você poderia simplificá-lo ainda mais assim:

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

Ou mesmo como uma linha usando uma função de seta, a destruição de objetos e o &&operador:

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

Aqui está uma versão atualizada do código fornecido pelo @theprtk . É um pouco limpo para mostrar a versão generalizada enquanto se tem um exemplo.

Nota: eu adicionaria isso como um comentário em sua postagem, mas ainda não tenho reputação suficiente

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

Você pode usar o método depois de você map(). O método, filter()por exemplo, no seu caso:

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

O filtro do método:

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

Em seguida, apenas os itens existentes estão na nova matriz sourceFiltered.

Cristhian D
fonte