Bagaimana cara melompati elemen dalam .map ()?

418

Bagaimana saya bisa melewatkan elemen array .map?

Kode saya:

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

Ini akan mengembalikan:

["img.png", null, "img.png"]
Ismail
sumber
19
Anda tidak bisa, tetapi Anda bisa menyaring semua nilai nol setelahnya.
Felix Kling
1
Kenapa tidak? Saya tahu menggunakan terus tidak berfungsi tetapi akan lebih baik untuk mengetahui mengapa (juga akan menghindari perulangan ganda) - sunting - untuk kasus Anda, bisakah Anda membalikkan kondisi if dan hanya kembali img.srcjika hasil dari pop split! = = json?
GrayedFox
@GrayedFox Maka implisit undefinedakan dimasukkan ke dalam array, bukan null. Tidak lebih baik ...
FZ

Jawaban:

640

Hanya .filter()itu yang pertama:

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

Jika Anda tidak ingin melakukan itu, yang tidak masuk akal karena memiliki biaya, Anda dapat menggunakan yang lebih umum .reduce(). Secara umum Anda dapat mengekspresikan .map()dalam hal .reduce:

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

dapat ditulis sebagai

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

Jadi jika Anda perlu melewati elemen, Anda dapat melakukannya dengan mudah dengan .reduce():

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

Dalam versi itu, kode dalam .filter()dari sampel pertama adalah bagian dari .reduce()panggilan balik. Sumber gambar hanya didorong ke array hasil dalam kasus di mana operasi filter akan menyimpannya.

Runcing
sumber
21
Bukankah ini mengharuskan Anda mengulang seluruh array dua kali? Apakah ada cara untuk menghindarinya?
Alex McMillan
7
@AlexMcMillan dapat Anda gunakan .reduce()dan lakukan semuanya dalam satu pass, meskipun menurut kinerja saya ragu itu akan membuat perbedaan yang signifikan.
Runcing
9
Dengan semua negatif ini, "mengosongkan" nilai-nilai -gaya ( null, undefined, NaNdll) itu akan baik jika kita bisa memanfaatkan salah satu di dalam map()sebagai indikator bahwa objek ini peta apa-apa dan harus dilewati. Saya sering menemukan array yang ingin saya petakan di 98% (misalnya: String.split()meninggalkan satu string kosong di akhir, yang tidak saya pedulikan). Terima kasih atas jawaban Anda :)
Alex McMillan
6
@AlexMcMillan well .reduce()adalah semacam-baseline "melakukan apa pun yang Anda inginkan" berfungsi, karena Anda memiliki kontrol penuh atas nilai kembali. Anda mungkin tertarik pada karya luar biasa oleh Rich Hickey di Clojure mengenai konsep transduser .
Runcing
3
@vsync Anda tidak dapat melewati elemen .map(). Namun Anda dapat menggunakannya .reduce(), jadi saya akan menambahkannya.
Runcing
25

Saya pikir cara paling sederhana untuk melewati beberapa elemen dari array adalah dengan menggunakan metode filter () .

Dengan menggunakan metode ini ( ES5 ) dan sintaks ES6 Anda dapat menulis kode Anda dalam satu baris , dan ini akan mengembalikan apa yang Anda inginkan :

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
sumber
1
itulah tepatnya .filter()dibuat untuk
avalanche1
2
Apakah ini lebih baik daripada forEachdan menyelesaikannya dalam satu pass, bukan dua?
wuliwong
1
Terserah Anda @wuliwong. Tetapi harap perhatikan bahwa ini masih O(n)dalam kompleksitas pengukuran dan silakan lihat setidaknya dua artikel ini juga: frontendcollisionblog.com/javascript/2015/08/15/… dan coderwall.com/p/kvzbpa/don-t- use-array-foreach-use-for-sebaliknya Semua yang terbaik!
simhumileco
1
@Simhumileco terima kasih! Tepat karena itu, saya di sini (dan mungkin banyak yang lain juga). Pertanyaannya mungkin adalah bagaimana menggabungkan .filter dan .map hanya dengan iterasi satu kali.
Jack Black
21

Sejak 2019, Array.prototype.flatMap adalah pilihan yang bagus.

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

Dari MDN :

flatMapdapat digunakan sebagai cara untuk menambah dan menghapus item (memodifikasi jumlah item) selama peta. Dengan kata lain, ini memungkinkan Anda untuk memetakan banyak item ke banyak item (dengan menangani setiap item input secara terpisah), daripada selalu satu per satu. Dalam hal ini, ini berfungsi seperti kebalikan dari filter. Cukup kembalikan array 1 elemen untuk menyimpan item, array multi elemen untuk menambahkan item, atau array 0 elemen untuk menghapus item.

Trevor Dixon
sumber
1
Jawaban terbaik dijatuhkan! Info lebih lanjut di sini: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Dominique PERETTI
1
inilah jawaban yang sebenarnya, sederhana dan cukup kuat. kami belajar ini lebih baik daripada filter dan kurangi.
membela orca
19

TLDR: Anda dapat memfilter array Anda terlebih dahulu dan kemudian melakukan peta Anda tetapi ini akan membutuhkan dua lintasan pada array (filter mengembalikan array ke peta). Karena array ini kecil, itu adalah biaya kinerja yang sangat kecil. Anda juga dapat melakukan pengurangan sederhana. Namun, jika Anda ingin membayangkan kembali bagaimana hal ini dapat dilakukan dengan satu melewati array (atau tipe data apa pun), Anda dapat menggunakan ide yang disebut "transduser" yang dipopulerkan oleh Rich Hickey.

Menjawab:

Kita seharusnya tidak memerlukan peningkatan dot chaining dan operasi pada array [].map(fn1).filter(f2)...karena pendekatan ini menciptakan array menengah dalam memori pada setiap reducingfungsi.

Pendekatan terbaik beroperasi pada fungsi pereduksi aktual sehingga hanya ada satu pass data dan tidak ada array tambahan.

Fungsi reduksi adalah fungsi yang dilewatkan ke dalam reducedan mengambil akumulator dan input dari sumber dan mengembalikan sesuatu yang tampak seperti akumulator

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

Sumber: posting transduser cupang kaya

terima kasih
sumber
17

Inilah solusi yang menyenangkan:

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

Gunakan dengan operator mengikat :

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
Mpen
sumber
1
Metode ini menyelamatkan saya dari keharusan untuk menggunakan terpisah map, filterdan concatpanggilan.
LogicalBranch
11

Jawaban tanpa kasus tepi berlebihan:

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

Mengapa tidak menggunakan forEach loop saja?

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

Atau bahkan gunakan filter yang lebih sederhana:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
sumber
1
Terbaik adalah simpel untuk loop yang memfilter & membuat array baru, tetapi untuk konteks penggunaan, mapbiarkan seperti sekarang. (adalah 4 tahun yang lalu saya menanyakan pertanyaan ini, ketika saya tidak tahu apa-apa tentang pengkodean)
Ismail
Cukup adil, mengingat bahwa tidak ada cara langsung ke atas dengan peta dan semua solusi menggunakan metode alternatif saya pikir saya akan chip dengan cara paling sederhana yang dapat saya pikirkan untuk melakukan hal yang sama.
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);

The .filter(Boolean)akan menyaring nilai-nilai falsey di array yang diberikan, yang dalam kasus Anda adalah null.

Lucas P.
sumber
3

Berikut adalah metode utilitas (kompatibel ES5) yang hanya memetakan nilai-nilai bukan nol (menyembunyikan panggilan untuk mengurangi):

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

Saya menggunakan .forEachuntuk mengulangi, dan mendorong hasil ke resultsarray kemudian menggunakannya, dengan solusi ini saya tidak akan mengulang array dua kali

SayJeyHi
sumber
1

Untuk meramalkan komentar Felix Kling , Anda dapat menggunakan .filter()seperti ini:

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

Itu akan menghapus nilai falsey dari array yang dikembalikan oleh .map()

Anda dapat menyederhanakannya lebih lanjut seperti ini:

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

Atau bahkan sebagai satu-liner menggunakan fungsi panah, perusakan objek dan &&operator:

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

Ini adalah versi terbaru dari kode yang disediakan oleh @theprtk . Ini adalah sedikit dibersihkan untuk menunjukkan versi umum sementara memiliki contoh.

Catatan: Saya akan menambahkan ini sebagai komentar pada postingannya tetapi saya belum memiliki reputasi yang cukup

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

Anda dapat menggunakan metode setelah Anda map(). Metode filter()misalnya dalam kasus Anda:

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

Filter metode:

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

Kemudian, hanya item yang ada dalam array baru sourceFiltered.

Cristhian D
sumber