837

How can I skip an array element in .map?

My code:

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

This will return:

["img.png", null, "img.png"]
Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Ismail
  • 8,904
  • 3
  • 21
  • 39
  • 51
    You can't, but you could filter out all null values afterwards. – Felix Kling Jul 17 '14 at 14:50
  • 1
    Why not? I know using [continue](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/continue) doesn't work but it would be good to know why (also would avoid double looping) - edit - for your case couldn't you just invert the if condition and only return `img.src` if the result of the split pop !== json? – GrayedFox Apr 25 '18 at 13:35
  • 1
    @GrayedFox Then implicit `undefined` would be put into the array, instead of `null`. Not that better... – FZs Apr 07 '20 at 17:27

19 Answers19

1191

Just .filter() it first:

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

If you don't want to do that, which is not unreasonable since it has some cost, you can use the more general .reduce(). You can generally express .map() in terms of .reduce:

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

can be written as

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

So if you need to skip elements, you can do that easily with .reduce():

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

In that version, the code in the .filter() from the first sample is part of the .reduce() callback. The image source is only pushed onto the result array in the case where the filter operation would have kept it.

update — This question gets a lot of attention, and I'd like to add the following clarifying remark. The purpose of .map(), as a concept, is to do exactly what "map" means: transform a list of values into another list of values according to certain rules. Just as a paper map of some country would seem weird if a couple of cities were completely missing, a mapping from one list to another only really makes sense when there's a 1 to 1 set of result values.

I'm not saying that it doesn't make sense to create a new list from an old list with some values excluded. I'm just trying to make clear that .map() has a single simple intention, which is to create a new array of the same length as an old array, only with values formed by a transformation of the old values.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • 50
    Doesn't this require you loop over the entire array twice? Is there any way to avoid that? – Alex McMillan Oct 28 '15 at 01:24
  • 10
    @AlexMcMillan you could use `.reduce()` and do it all in one pass, though performance-wise I doubt it'd make a significant difference. – Pointy Oct 28 '15 at 02:10
  • 13
    With all these negative, "empty"-style values (`null`, `undefined`, `NaN` etc) it would be good if we could utilise one inside a `map()` as an indicator that this object maps to nothing and should be skipped. I often come across arrays I want to map 98% of (eg: `String.split()` leaving a single, empty string at the end, which I don't care about). Thanks for your answer :) – Alex McMillan Oct 28 '15 at 02:54
  • 9
    @AlexMcMillan well `.reduce()` is sort-of the baseline "do whatever you want" function, because you have complete control over the return value. You might be interested in the excellent work by Rich Hickey in Clojure concerning the concept of [transducers](https://www.youtube.com/watch?v=6mTbuzafcII). – Pointy Oct 28 '15 at 04:29
  • 1
    I came here (from google) because I want to skip an element, not to use filter, which is something else entirely.. can you please also answer the direct title? thank you – vsync Oct 14 '16 at 23:17
  • 4
    @vsync you can't skip an element with `.map()`. You can however use `.reduce()` instead, so I'll add that. – Pointy Oct 14 '16 at 23:17
  • I came across a better answer - http://stackoverflow.com/a/27110357/104380 – vsync Oct 14 '16 at 23:19
  • 1
    @AlexMcMillan but what if you *want* a `null`? What if you need to add more than one result per element? Generators with `yield` would be the end of all these problems. – transistor09 Oct 15 '16 at 14:23
  • 1
    Those who want to avoid looping over the array twice seem to miss that what counts is the total number of operations. For 10 elements: filter operations + map operations = 20 operations. If you can do it in one loop, you still have to do a (filter check + a map operation) * 10 = still 20 operations. There could be some overhead to the actual map/filter function call but it's probably next to nothing, and the wrong thing to optimise. – laurent Feb 27 '18 at 10:25
  • @this.lau_ Actually this is false: https://jsperf.com/reduce-vs-mapfilter/1 – Bastien Mar 12 '18 at 16:58
  • @Bastien in Firefox, the map + filter test is over twice as fast as the reduce version, though both are sufficiently fast that it makes no practical difference. – Pointy Mar 12 '18 at 17:03
  • Reduce was a great option for me, filtering (to my knowledge), didn't allow for modification of the resulting elements. – Jarrod L Nov 22 '18 at 19:39
  • 2
    @Pointy `if (predicate) return false else return true` - seriously?! ;-) – Alnitak Jan 08 '19 at 09:29
  • @Alnitak of course I don't remember crafting the example in the answer but I'll assume I did it for clarity :) – Pointy Jan 08 '19 at 14:01
  • I was trying to write a reducer (immutable) that removed an item from a particular index of an array, and this solution of chaining `filter` and `map` works perfectly for this situation. Cheers! – Rafael Marques May 07 '20 at 19:36
  • Instead of using `.map()` which makes more sense in a 1 to 1 setting, or unnaturally trying to use `.reduce()`... Simply use a `for ... in` or `for ... of` loop! – Grateful Dec 24 '20 at 04:34
  • 2
    See the next answer and use `.flatMap()` instead. – Viacheslav Dobromyslov Feb 19 '21 at 16:04
317

Since 2019, Array.prototype.flatMap is a good option.

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

From MDN:

flatMap can be used as a way to add and remove items (modify the number of items) during a map. In other words, it allows you to map many items to many items (by handling each input item separately), rather than always one-to-one. In this sense, it works like the opposite of filter. Simply return a 1-element array to keep the item, a multiple-element array to add items, or a 0-element array to remove the item.

Trevor Dixon
  • 23,216
  • 12
  • 72
  • 109
  • 23
    Best answer hands down! More info here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap#For_adding_and_removing_items_during_a_map – Dominique PERETTI Dec 28 '19 at 18:47
  • 11
    this is the really answer, simple and strong enough. we learn this is better than filter and reduce. – defend orca Mar 27 '20 at 18:00
  • 2
    great, but I wish there was a native method without the need to return the empty array - maybe a method that would put all truthy values in the new array and skip falsy ones. – Björn Aug 06 '20 at 15:08
  • 10
    First, props to MDN for providing this kind of commentary. It is not common that documentation has this sort of practical use case examples. Second, I do wish it was more specific about the _slightly more efficient_ part. How much more efficient than `map` followed by `flat`? – maletor Oct 27 '20 at 16:32
  • 5
    Wonderful function! Shorter modern syntax with nullish coalescing: foo.flatMap(bar => bar.baz ?? [ ]) – n4ks Aug 09 '21 at 14:11
  • **For the record** if anyone is using Lodash (& lodash/fp) flatmap, you can also use this exact behaviour. Also, MDN is a treasure. – sab Aug 11 '21 at 15:19
  • 2
    I vote for this solution. Regarding PERFORMANCES, It is logical that its one round is faster than 2 rounds of filter + map actions, unless its 'flattening' part causes performance downgrading – Sinisa Rudan Apr 19 '22 at 11:31
  • 1
    What's done under the hood? You sure this is happening in one round? Also, the code doesn't read very obvious if you don't understand how this function works. IMHO calling `.filter(Boolean)` is more explicit. – enapupe Oct 04 '22 at 14:43
  • 1
    This solution appears to be more type safe as well. With filter().map() typescript didn't seem to be able to figure out what was excluded properly. – Justin Caldicott Jun 07 '23 at 20:39
41

I think the most simple way to skip some elements from an array is by using the filter() method.

By using this method (ES5) and the ES6 syntax you can write your code in one line, and this will return what you want:

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);
FZs
  • 16,581
  • 13
  • 41
  • 50
simhumileco
  • 31,877
  • 16
  • 137
  • 115
  • 1
    that's exactly what `.filter()` was made for – avalanche1 Nov 27 '18 at 19:13
  • 3
    Is this better than `forEach` and completing it in one pass instead of two? – wuliwong Oct 01 '19 at 23:57
  • 1
    As you wish @wuliwong. But please take into account that this will be still `O(n)` in complexity meassure and please look at least at these two articles too: http://frontendcollisionblog.com/javascript/2015/08/15/3-reasons-you-should-not-be-using-foreach.html and https://coderwall.com/p/kvzbpa/don-t-use-array-foreach-use-for-instead All the best! – simhumileco Oct 02 '19 at 05:37
  • 1
    Thank you @simhumileco! Exactly because of that, I am here (and probably many others as well). The question is probably how to combine .filter and .map by only iterating once. – Marc Jan 22 '20 at 13:43
27

TLDR: You can first filter your array and then perform your map but this would require two passes on the array (filter returns an array to map). Since this array is small, it is a very small performance cost. You can also do a simple reduce. However if you want to re-imagine how this can be done with a single pass over the array (or any datatype), you can use an idea called "transducers" made popular by Rich Hickey.

Answer:

We should not require increasing dot chaining and operating on the array [].map(fn1).filter(f2)... since this approach creates intermediate arrays in memory on every reducing function.

The best approach operates on the actual reducing function so there is only one pass of data and no extra arrays.

The reducing function is the function passed into reduce and takes an accumulator and input from the source and returns something that looks like the accumulator

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

Resources: rich hickey transducers post

theptrk
  • 730
  • 9
  • 17
  • but isn't `.concat()` going to be repeatedly copying `acc`, making it O(n^2)? – netotz Dec 10 '22 at 00:15
  • Yeah, it's more of a brain exercise in javascript where clojure (from the linked Rich Hickey post) would support functional operations like this. – theptrk Dec 10 '22 at 20:55
25

Here's a fun solution:

/**
 * Filter-map. Like map, but skips undefined values.
 *
 * @param callback
 */
function fmap(callback) {
    return this.reduce((accum, ...args) => {
        const x = callback(...args);
        if(x !== undefined) {
            accum.push(x);
        }
        return accum;
    }, []);
}

Use with the bind operator:

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]
Abdul Mahamaliyev
  • 750
  • 1
  • 8
  • 20
mpen
  • 272,448
  • 266
  • 850
  • 1,236
20

Why not just use a forEach loop?

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

Or even simpler use filter:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];
Alex
  • 2,651
  • 2
  • 25
  • 45
  • 1
    Best would be a simple for loop that filters & creates an new array, but for the context of using `map` lets keep it like it's now. (was 4yrs ago I asked this question, when I knew nothing about coding) – Ismail Jun 27 '18 at 10:21
  • Fair enough, given that there is no direct way to the above with map and all the solutions used an alternative method I thought I would chip in the simplest way I could think of to do the same. – Alex Jun 27 '18 at 10:39
15

Answer sans superfluous edge cases:

const thingsWithoutNulls = things.reduce((acc, thing) => {
  if (thing !== null) {
    acc.push(thing);
  }
  return acc;
}, [])
Thiago Mata
  • 2,825
  • 33
  • 32
corysimmons
  • 7,296
  • 4
  • 57
  • 65
12
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) will filter out any falsey values in a given array, which in your case is the null.

Lucas P.
  • 180
  • 1
  • 6
10

To extrapolate on Felix Kling's comment, you can use .filter() like this:

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

That will remove falsey values from the array that is returned by .map()

You could simplify it further like this:

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

Or even as a one-liner using an arrow function, object destructuring and the && operator:

var sources = images.map(({ src }) => src.split('.').pop() !== "json" && src).filter(Boolean);
camslice
  • 571
  • 6
  • 8
4

Here's a utility method (ES5 compatible) which only maps non null values (hides the call to reduce):

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
  • 2,669
  • 23
  • 35
4

const arr = [0, 1, '', undefined, false, 2, undefined, null, , 3, NaN];
const filtered = arr.filter(Boolean);
console.log(filtered);

/*
    Output: [ 1, 2, 3 ]
*/
Nick Vu
  • 14,512
  • 4
  • 21
  • 31
Matt
  • 41
  • 1
3

if it null or undefined in one line ES5/ES6

//will return array of src 
images.filter(p=>!p.src).map(p=>p.src);//p = property


//in your condition
images.filter(p=>p.src.split('.').pop() !== "json").map(p=>p.src);
Hisham
  • 1,279
  • 1
  • 17
  • 23
1

I use .forEach to iterate over , and push result to results array then use it, with this solution I will not loop over array twice

SayJeyHi
  • 1,559
  • 4
  • 19
  • 39
1

You can use after of you method map(). The method filter() for example in your case:

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

The method filter:

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

Then, only the existing items are in the new array sourceFiltered.

Azametzin
  • 5,223
  • 12
  • 28
  • 46
Cristhian D
  • 572
  • 6
  • 5
0

Here is a updated version of the code provided by @theprtk. It is a cleaned up a little to show the generalized version whilst having an example.

Note: I'd add this as a comment to his post but I don't have enough reputation yet

/**
 * @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
  • 171
  • 1
  • 4
0

You can do this

var sources = [];
images.map(function (img) {
    if(img.src.split('.').pop() !== "json"){ // if extension is not .json
        sources.push(img.src); // just push valid value
    }
});
herman
  • 33
  • 2
  • 2
    This is not fit with `map`. Instead you can use `forEach` or `reduce` like in the selected answer – nrofis Oct 29 '20 at 20:24
  • The point of Array.map() is that it returns the result. So the above suggestion is not very readable. – OlleMattsson Sep 29 '21 at 08:05
  • In Perl one can do something simple like this: [ "first", "a" eq "ab" ? "second" : () ] I can't find (so far) anything as simple as that in javascript. – Tim Potapov Oct 04 '21 at 14:38
0

I use foreach():

var sources = [];

images.forEach(function (img) {
    if(img.src.split('.').pop() !== "json"){ // if extension is .json
        sources.push(img);
    }
});

NOTE: I negated your logic.

Designly
  • 266
  • 1
  • 9
0

you can use map + filter like this :

   var sources = images.map(function (img) {
    if(img.src.split('.').pop() === "json"){ // if extension is .json
        return null; // skip
    }
    else{
        return img.src;
    }})?.filter(x => x !== null);
Salih ŞEKER
  • 199
  • 1
  • 3
-3

Just use .filter() after .map(). This is the smartest way in my opinion.

var sources = images.map(function (img) {
    if(img.src.split('.').pop() === "json"){ // if extension is .json
        return null; // skip
    }
    else{
        return img.src;
    }
}).filter(x => x);
Manuel
  • 2,334
  • 4
  • 20
  • 36