map, filter and reduce
Most of what you do with an array is one of three things: transform every item, keep some of them, or boil the whole lot down to a single value. Each has a method, and using the right one makes the intention visible in the first word.
const prices = [250, 120, 80];
console.log(prices.map(p => p * 2));
console.log(prices.filter(p => p < 200));
console.log(prices.reduce((sum, p) => sum + p, 0));
[ 500, 240, 160 ] [ 120, 80 ] 450
Three lines, three questions answered. Written as loops that is fifteen lines and three chances to get the counter wrong.
map transforms
map runs a function on every item and returns a new array of the results.
It is always the same length as the original.
const names = ["ayesha", "bilal"];
console.log(names.map(n => n.toUpperCase()));
console.log(names.map(n => n.length));
console.log(names);
[ 'AYESHA', 'BILAL' ] [ 6, 5 ] [ 'ayesha', 'bilal' ]
The original is untouched — map copies. That is the property that makes these
methods chainable and safe.
The function gets the index as a second argument when you need it:
const items = ["chai", "paratha"];
console.log(items.map((item, i) => `${i + 1}. ${item}`));
[ '1. chai', '2. paratha' ]
The commonest map mistake
An arrow function with braces needs an explicit return. Without one it
returns undefined, and map dutifully collects those.
const prices = [250, 120];
console.log(prices.map(p => { p * 2 }));
console.log(prices.map(p => { return p * 2; }));
console.log(prices.map(p => p * 2));
[ undefined, undefined ] [ 500, 240 ] [ 500, 240 ]
p => p * 2 returns automatically because there are no braces. The moment you
add braces you have written a function body, and a body returns nothing unless
told to. An array full of undefined is almost always this.
filter keeps
filter runs a test on every item and returns a new array of the ones that
passed. It can be shorter than the original, or empty.
const marks = [72, 45, 88, 30];
console.log(marks.filter(m => m >= 50));
console.log(marks.filter(m => m > 100));
console.log(marks.filter(m => m >= 50).length);
[ 72, 88 ] [] 2
The function must return a boolean-ish value. Returning the item itself is a classic slip:
const values = [0, 1, 2, "", "chai"];
console.log(values.filter(v => v));
[ 1, 2, 'chai' ]
That kept every truthy value and dropped 0 and "", which is occasionally
what you want and usually a bug — the same falsy trap as everywhere else.
reduce boils down
reduce walks the array carrying an accumulator, and returns whatever the
accumulator ends up as.
const prices = [250, 120, 80];
console.log(prices.reduce((sum, price) => sum + price, 0));
console.log(prices.reduce((max, price) => (price > max ? price : max), 0));
450 250
The two arguments to reduce are the function and the starting value. That
second one is easy to forget and matters:
console.log([].reduce((a, b) => a + b, 0));
0
Without the 0, reducing an empty array throws TypeError: Reduce of empty array with no initial value. Always supply it.
reduce is not only for numbers. Counting occurrences is the useful case:
const orders = ["chai", "coffee", "chai"];
const counts = orders.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {});
console.log(counts);
{ chai: 2, coffee: 1 }Note the return acc — the braces are back, so the return is required. This is
where the earlier mistake bites hardest, because forgetting it makes the
accumulator undefined on the second pass and the error appears deep inside.
If a reduce is getting hard to read, a plain loop is the better answer. It is
the most powerful of the three and the easiest to overuse.
Chaining
Because map and filter return arrays, they compose:
const students = [
{ name: "Ayesha", marks: 72 },
{ name: "Bilal", marks: 45 },
{ name: "Sana", marks: 88 },
];
const passed = students
.filter(s => s.marks >= 50)
.map(s => s.name)
.join(", ");
console.log(passed);
Ayesha, Sana
Read it top to bottom as a sentence: keep the ones who passed, take their names, join them. Filter before you map — transforming items you are about to throw away is wasted work.
Finding, testing and searching
const students = [
{ name: "Ayesha", marks: 72 },
{ name: "Bilal", marks: 45 },
];
console.log(students.find(s => s.marks < 50));
console.log(students.findIndex(s => s.marks < 50));
console.log(students.some(s => s.marks >= 70));
console.log(students.every(s => s.marks >= 70));
console.log([1, 2, 3].includes(2));
{ name: 'Bilal', marks: 45 }
1
true
false
truefindreturns the first matching item, orundefined.findIndexreturns its position, or-1.someandeveryreturn booleans and stop as soon as the answer is known.includeschecks for a value, not a condition.
find versus filter is worth being deliberate about: filter always walks
the whole array and returns a list; find stops at the first hit and returns
the item. When you want one thing, find says so.
Which methods copy and which mutate
This is the distinction that causes real bugs, because the mutating ones change your array without saying so.
Copy — map, filter, slice, concat, toSorted, toReversed.
Mutate — sort, reverse, push, pop, splice.
const original = [3, 1, 2];
const sorted = original.sort();
console.log(sorted);
console.log(original);
[ 1, 2, 3 ] [ 1, 2, 3 ]
sort sorted the original in place and returned the same array, so both
names now point at one sorted array. If something else was relying on the
original order, it has silently changed.
Copy first:
const original = [3, 1, 2];
const sorted = [...original].sort();
console.log(sorted);
console.log(original);
[ 1, 2, 3 ] [ 3, 1, 2 ]
sort compares text by default
console.log([10, 9, 100].sort());
console.log([10, 9, 100].sort((a, b) => a - b));
[ 10, 100, 9 ] [ 9, 10, 100 ]
With no comparison function, sort converts everything to strings and sorts
alphabetically — so "100" comes before "9". It is not broken; it is
documented and surprising.
Pass (a, b) => a - b for ascending numbers, b - a for descending. For
strings, (a, b) => a.localeCompare(b).
forEach, and why it is last
forEach runs a function for each item and returns nothing.
["chai", "paratha"].forEach(item => console.log(item));
chai paratha
It cannot be chained, and — the real limitation — it cannot be stopped.
break is a syntax error inside it, and return only exits that one call.
[1, 2, 3, 4].forEach(n => {
if (n === 3) return;
console.log(n);
});
1 2 4
The return skipped 3 and carried on. If you need to stop early, use
for...of with break, or find / some, which stop by design.
Reach for forEach only when you genuinely want a side effect for every item
and nothing back. If you are building an array inside one, you wanted map.
Choosing
| You want | Method |
|---|---|
| Same length, changed items | map |
| Fewer items, unchanged | filter |
| One value from many | reduce |
| The first match | find |
| Yes or no | some / every / includes |
| A side effect, no result | forEach |
| To stop partway | for...of with break |
Next: objects, which is what the items in most real arrays turn out to be.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes