Как пропустить элемент в .map ()?

418

Как я могу пропустить элемент массива в .map ?

Мой код:

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

Это вернет:

["img.png", null, "img.png"]
Ismail
источник
19
Вы не можете, но вы можете отфильтровать все нулевые значения впоследствии.
Феликс Клинг
1
Почему бы нет? Я знаю, что использование continue не работает, но было бы хорошо узнать, почему (также можно было бы избежать двойного зацикливания) - edit - для вашего случая вы не могли бы просто инвертировать условие if и возвращать только img.srcесли результат разделения pop! = = JSON?
GrayedFox
@GrayedFox Тогда неявный undefinedбудет помещен в массив, а не null. Не так уж и лучше ...
ФЗ

Ответы:

640

Просто .filter()сначала:

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

Если вы не хотите этого делать, что не является необоснованным, поскольку имеет определенную стоимость, вы можете использовать более общее .reduce(). Как правило, вы можете выразить .map()в терминах .reduce:

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

можно записать как

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

Поэтому, если вам нужно пропустить элементы, вы можете легко это сделать с помощью .reduce():

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

В этой версии код .filter()из первого примера является частью .reduce()обратного вызова. Источник изображения помещается в массив результатов только в том случае, если операция фильтра сохранила бы его.

Заостренный
источник
21
Разве это не требует, чтобы вы дважды перебирали весь массив? Есть ли способ избежать этого?
Алекс Макмиллан
7
@AlexMcMillan вы могли бы использовать .reduce()и делать все это за один проход, хотя в плане производительности я сомневаюсь, что это будет иметь существенное значение.
Заостренный
9
При всех этих отрицательных значениях «пустого» стиля ( null, undefinedи NaNт. Д.) Было бы хорошо, если бы мы могли использовать единицу map()в качестве индикатора того, что этот объект ничего не отображает и его следует пропустить. Я часто сталкиваюсь с массивами, которые я хочу отобразить на 98% (например, String.split()оставляя одну пустую строку в конце, которая меня не волнует). Спасибо за ваш ответ :)
Алекс Макмиллан
6
@AlexMcMillan .reduce()- это своего рода базовая функция «делай что хочешь», потому что у тебя есть полный контроль над возвращаемым значением. Возможно, вас заинтересует отличная работа Rich Hickey в Clojure, касающаяся концепции преобразователей .
Заостренный
3
@vsync, с которым нельзя пропустить элемент .map(). Однако вы можете использовать .reduce()вместо этого, поэтому я добавлю это.
Заостренный
25

Я думаю, что самый простой способ пропустить некоторые элементы из массива - использовать метод filter () .

Используя этот метод ( ES5 ) и синтаксис ES6, вы можете написать свой код в одну строку , и это вернет то, что вы хотите :

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
источник
1
это именно то, что .filter()было сделано для
лавина1
2
Это лучше, чем forEachзавершить его за один проход вместо двух?
Вуливонг
1
Как пожелаешь @wuliwong. Но, пожалуйста, примите во внимание, что это все еще будет O(n)сложно измерить, и, пожалуйста, посмотрите хотя бы на эти две статьи: frontendcollisionblog.com/javascript/2015/08/15/… и coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-Вместо всего наилучшего!
Simhumileco
1
Спасибо @simhumileco! Именно поэтому я здесь (и, вероятно, многие другие). Вопрос, вероятно, заключается в том, как объединить .filter и .map, выполняя только одну итерацию.
Джек Блэк
21

С 2019 года Array.prototype.flatMap является хорошим вариантом.

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

От MDN :

flatMapможет использоваться как способ добавления и удаления элементов (изменения количества элементов) во время карты. Другими словами, он позволяет сопоставить множество элементов множеству элементов (обрабатывая каждый элемент ввода отдельно), а не всегда один к одному. В этом смысле он работает как противоположность фильтра. Просто верните массив из 1 элемента, чтобы сохранить элемент, массив из нескольких элементов, чтобы добавить элементы, или массив из 0 элементов, чтобы удалить элемент.

Тревор Диксон
источник
1
Лучший ответ руки вниз! Более подробная информация здесь: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Доминик ПЕРЕТТИ,
1
это действительно ответ, простой и достаточно сильный. мы узнаем, что это лучше, чем фильтровать и уменьшать.
защищать орка
19

TLDR: вы можете сначала отфильтровать массив, а затем выполнить карту, но для этого потребуется два прохода в массиве (фильтр возвращает массив в карту). Поскольку этот массив небольшой, это очень низкая стоимость производительности. Вы также можете сделать простое сокращение. Однако, если вы хотите переосмыслить, как это можно сделать за один проход по массиву (или любому другому типу данных), вы можете использовать идею под названием «преобразователи», ставшую популярной благодаря Ричу Хикки.

Ответ:

Мы не должны требовать увеличения цепочки точек и работы с массивом, [].map(fn1).filter(f2)...так как этот подход создает промежуточные массивы в памяти на каждомreducing функции.

Наилучший подход основан на действительной функции сокращения, поэтому существует только один проход данных и никаких дополнительных массивов.

Редукционная функция - это функция, передаваемая reduceи получающая аккумулятор и входные данные от источника и возвращающая нечто, похожее на аккумулятор

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

Ресурсы: богатый пост преобразователей хикки

theptrk
источник
17

Вот забавное решение:

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

Используйте с оператором связывания :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
mpen
источник
1
Этот метод спас меня от того , чтобы использовать отдельные map, filterи concatзвонки.
LogicalBranch
11

Ответьте без лишних краевых случаев:

const thingsWithoutNulls = things.reduce((acc, thing) => {
  if (thing !== null) {
    acc.push(thing);
  }
  return acc;
}, [])
corysimmons
источник
10

Почему бы просто не использовать цикл 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'];

Или еще проще использовать фильтр:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
источник
1
Лучше всего будет простой цикл for, который фильтрует и создает новый массив, но для контекста использования mapпозволяет сохранить его таким, как сейчас. (4 года назад я задавал этот вопрос, когда ничего не знал о кодировании)
Исмаил
Справедливо, учитывая, что с картой нет прямого пути к вышесказанному, и во всех решениях использовался альтернативный метод, который, как я думал, я выбрал бы самым простым способом, о котором я мог подумать, сделать то же самое.
Алекс
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);

.filter(Boolean)Отфильтрует любые значения falsey в данном массиве, который в вашем случае является null.

Лукас П.
источник
3

Вот вспомогательный метод (совместимый с ES5), который отображает только ненулевые значения (скрывает призыв к сокращению):

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
источник
1

Я использую, .forEachчтобы перебрать и передать результат в resultsмассив, а затем использовать его, с этим решением я не буду перебирать массив дважды

SayJeyHi
источник
1

Чтобы экстраполировать комментарий Феликса Клинга , вы можете использовать .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);

Это удалит ложные значения из массива, который возвращается .map()

Вы могли бы упростить это далее так:

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

Или даже в виде однострочника с использованием функции стрелки, деструктуризации объекта и &&оператора:

var sources = images.map(({ src }) => src.split('.').pop() !== "json" && src).filter(Boolean);
camslice
источник
0

Вот обновленная версия кода, предоставленного @theprtk . Это немного исправлено, чтобы показать обобщенную версию, имея пример.

Примечание: я бы добавил это как комментарий к своему посту, но у меня пока недостаточно репутации

/**
 * @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
источник
0

Вы можете использовать после вас метод map(). Метод filter()например в вашем случае:

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

Метод фильтра:

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

Тогда только существующие элементы находятся в новом массиве sourceFiltered.

Cristhian D
источник