The three that do most of the work
mapalways returns an array of the same length. If you are dropping items, you wantfilter.filterkeeps the items whose callback is truthy.reducefolds a list into one value — a number, a string, an object, another array.
const doors = [
{ name: "Painted", days: 5, archived: false },
{ name: "Veneer", days: 10, archived: false },
{ name: "Ceramic", days: 15, archived: true },
];
doors.map((d) => d.name); // ["Painted","Veneer","Ceramic"]
doors.filter((d) => !d.archived); // two objects
doors.reduce((sum, d) => sum + d.days, 0); // 30
reduce, properly
// a total
doors.reduce((sum, d) => sum + d.days, 0);
// the best one — no initial value, so it starts from the first item
doors.reduce((best, d) => (d.days < best.days ? d : best));
// group into an object
doors.reduce((acc, d) => {
const key = d.days < 10 ? "fast" : "slow";
(acc[key] ??= []).push(d);
return acc;
}, {});
// or just use the built-in
Object.groupBy(doors, (d) => (d.days < 10 ? "fast" : "slow"));
Without it, reduce on an empty array throws. With it, an empty array simply gives you the initial value — which is almost always what you want.
Finding and testing
some on an empty array is false; every on an empty array is true. That second one surprises people, and it is mathematically correct: nothing failed.
doors.find((d) => d.days > 10); // the item, or undefined
doors.findIndex((d) => d.days > 10); // the index, or -1
doors.findLast((d) => !d.archived);
doors.some((d) => d.archived); // true if any
doors.every((d) => d.days > 0); // true if all
doors.includes(x); // by identity — for primitives
doors.indexOf(x);
Changing versus returning
const sorted = doors.sort((a, b) => a.days - b.days);
// doors is ALSO sorted now — sort works in place and returns the same array
const sorted = [...doors].sort((a, b) => a.days - b.days); // a copy
const sorted = doors.toSorted((a, b) => a.days - b.days); // the modern way
| Changes the original | Returns a new array |
|---|---|
push pop shift unshift | concat slice map filter flat |
splice sort reverse fill | toSorted toReversed toSpliced with |
[10, 9, 1].sort() gives [1, 10, 9]. Always pass (a, b) => a - b for numbers and (a, b) => a.localeCompare(b) for text.
Spread and destructuring
A spread copy is shallow: the new array is new, the objects inside it are the same objects. Changing copy[0].name changes the original too.
const copy = [...list]; // a shallow copy
const joined = [...a, ...b];
const withExtra = [...list, item]; // instead of push, when you want a new array
const [first, second, ...rest] = list;
const [, , third] = list; // skip with commas
const [x = 0] = []; // a default for a missing item
// swap
[a, b] = [b, a];
Building arrays
Array.from({ length: 5 }, (_, i) => i * 2); // [0,2,4,6,8]
Array.from(document.querySelectorAll("li")); // a real array from a NodeList
[...new Set(list)]; // unique values
Array(3).fill(0); // [0,0,0]
"a,b,c".split(",");
It is three holes, and map skips holes entirely. Array(3).map((_, i) => i) gives you three holes back. Use Array.from({ length: 3 }, …).
Flattening
[[1, 2], [3, [4]]].flat(); // [1, 2, 3, [4]]
[[1, 2], [3, [4]]].flat(2); // [1, 2, 3, 4]
[[1, 2], [3]].flat(Infinity);
// map then flatten one level — good for "zero or more per item"
doors.flatMap((d) => (d.archived ? [] : [d.name]));
Chaining, and when to stop
Each step in a chain walks the whole array. For a few hundred items that is irrelevant. For a hundred thousand, one loop that does all three beats three passes — but measure before you make that trade, because the chain is far easier to read.
const summary = doors
.filter((d) => !d.archived)
.sort((a, b) => a.days - b.days)
.map((d) => `${d.name}: ${d.days}d`)
.join(" · ");