JavaScript60 min total · 16 parts
JavaScript Array and Object Methods Cheat Sheet
Part 3 of 16 · ~4 min
Transforming Arrays
Building an array before you transform it
Sometimes candidates doesn't start out as a friendly array — it starts as something array-like: a NodeList from the DOM, an arguments object, or just a length you want to fill. Three static methods handle that before any of the transformation methods below even apply:
Array.from(document.querySelectorAll(".candidate-row")); // NodeList → a real array with .map, .filter, etc.
Array.from({ length: 5 }, (_, i) => `seat-${i}`); // build one from nothing, using a mapping function
// ["seat-0", "seat-1", "seat-2", "seat-3", "seat-4"]
Array.of(7); // [7] — one element, the number 7
Array(7); // [ <7 empty items> ] — NOT the same thing, a completely different constructor call
Array.isArray(candidates); // true — the only reliable check; typeof candidates is just "object"
Array.of(7) and Array(7) look like they should do the same thing and don't: Array(7) with a single number argument sets the length to 7 and leaves seven empty slots, while Array.of(7) always treats its arguments as elements. If you've ever been confused by new Array(count) producing something you couldn't .map() over, that's why — and it's the reason Array.from({ length: n }, mapFn) is the more reliable way to build an array of a known size.
The core transformation methods
Once you have a real array, these are the ones that reshape it:
| Method | What you get back | Original array | Try it on candidates |
|---|---|---|---|
.map() | new array, one output per input | untouched | candidates.map(c => c.name) → 5 names |
.filter() | new array of the matches | untouched | candidates.filter(c => c.status === "active") → 3 candidates |
.reduce() | one folded-down value | untouched | candidates.reduce((sum, c) => sum + c.score, 0) → 374 |
.flat(depth) | flattened copy, depth defaults to 1 | untouched | [["node"], ["react", "css"]].flat() → ["node", "react", "css"] |
.flatMap() | runs .map(), then collapses one level of nesting out of the result | untouched | candidates.flatMap(c => c.tags) → all 10 tags, one flat list |
.sort(compareFn) | same array, reordered | mutated | candidates.sort((a, b) => a.score - b.score) |
.reverse() | same array, reversed | mutated | candidates.reverse() |
.slice(start, end) | shallow copy of a range | untouched | candidates.slice(0, 2) → Priya and Diego |
.splice(start, count, ...items) | the removed items; edits happen in place | mutated | candidates.splice(1, 1) → removes Diego, returns [Diego] |
.slice() and .splice() are the pair everyone mixes up, purely because the names are one letter apart. .slice() only ever reads — it hands you a copy of a stretch of the array and never touches the original. .splice() reaches into the live array and rewrites it directly: elements come out, new ones can go in at the same spot, sometimes both happen in a single call.
The default .sort() trap
Call .sort() with no comparator and it doesn't compare numbers as numbers — it turns every element into text first, then compares that text character by character:
const years = candidates.map(c => c.yearsExperience); // [8, 3, 12, 1, 10]
years.sort();
// [1, 10, 12, 3, 8] — NOT ascending numeric order
It makes sense once you stop thinking about size and start thinking about characters. The comparison walks "1" against "10" one character at a time, and "1" runs out of characters before "10" does — a tie goes to whichever string is shorter, so it sorts first. "3" only ends up after "1", "10", and "12" because the character '3' simply comes later than '1' does. Actual numeric size never once enters into the comparison.
years.sort((a, b) => a - b); // [1, 3, 8, 10, 12] — correct ascending
years.sort((a, b) => b - a); // [12, 10, 8, 3, 1] — correct descending
Make it a rule rather than something you remember case by case: sorting numbers always gets an explicit comparator. Write (a, b) => a - b when you want it ascending; swap which side gets subtracted and you have descending. Leave .sort() bare only when strings, ordered alphabetically, are genuinely the goal.
One more thing worth knowing, since it's easy to assume the opposite: .sort() has been a stable sort since 2019, meaning elements that compare equal keep their original relative order rather than shuffling unpredictably. Sort candidates by role and the two backend candidates that also tie on some secondary property will still come out in whatever order they were already in — you don't need a tiebreaker clause just to preserve that.