Як пропустити елемент у .map ()?


417

Як я можу пропустити елемент масиву в .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"]

18
Ви не можете, але ви могли відфільтрувати всі нульові значення згодом.
Фелікс Клінг

1
Чому ні? Я знаю, що використання продовжувати не працює, але було б добре знати, чому (також уникне подвійного циклічного циклу) - редагувати - для вашого випадку ви не могли просто перевернути умову if і повернутись лише у img.srcвипадку, якщо результат спліт-попу! = = json?
GrayedFox

@GrayedFox Тоді неявна undefinedбуде вставлена ​​в масив замість null. Не так краще ...
FZs

Відповіді:


637

Просто .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()та робити це все за один прохід, хоча, напевно, я думаю, що це може суттєво змінитись .
Pointy

9
З усіма цими негативними, "порожніми" значеннями стилю ( null, undefinedі NaNт. Д.) Було б добре, якби ми могли використовувати одне всередині a map()як індикатор того, що цей об'єкт не позначається нічим і його слід пропустити. Я часто натрапляю на масиви, на які я хочу зобразити 98% (наприклад: String.split()залишити в кінці єдиний порожній рядок, який мені не байдужий). Дякую за вашу відповідь :)
Alex McMillan

6
@AlexMcMillan добре налаштований .reduce()на базову лінію "роби все, що хочеш", тому що ти маєш повний контроль над поверненим значенням. Можливо, вас зацікавить чудова робота Ріга Хікі в Clojure, що стосується концепції перетворювачів .
Pointy

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


1
саме для цього .filter()було зроблено
лавина1

2
Це краще, ніж forEachі виконати його за один прохід замість двох?
wuliwong

1
Як хочеш @wuliwong. Але врахуйте, що це все ще буде O(n)складною мірою, і, будь ласка, подивіться принаймні на ці дві статті: frontendcollisionblog.com/javascript/2015/08/15/… та coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-namesto Все найкраще!
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
це справді відповідь, досить проста і досить сильна. ми дізнаємося, що це краще, ніж фільтрувати та зменшувати.
захищати orca

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

Ресурси: розміщуються багаті перетворювачі хіків


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]

1
Цей метод врятував мене від того , щоб використовувати окремі map, filterі concatдзвінки.
LogicalBranch

11

Відповідь не має зайвих крайових справ:

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

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'];

1
Найкраще було б простий цикл, який фільтрує та створює новий масив, але в контексті використання 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"]


1

Я використовую, .forEachщоб перебирати і натискати результат на resultsмасив, а потім використовувати його, з цим рішенням я не буду перебирати масив двічі


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

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

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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.