Technology  /  JavaScript

๐ŸŸจ JavaScript 15 guides ยท updated 2026

Closures, the event loop, promises, and modern ES syntax โ€” the language of the web explained the way it actually behaves at runtime.

JavaScript Array Methods: map, filter, reduce, and the Mutation Traps

Modern JavaScript style is built on array methods. Where older code looped with counters and accumulated into temp variables, todayโ€™s code declares intent: transform this list, keep these items, boil this down to one value. Reading and writing this style fluently is non-negotiable โ€” itโ€™s the dominant idiom in every codebase, framework, and code review youโ€™ll touch.

This guide covers the core methods with judgment (including when a plain loop is still right), reduce without the mystique, the sort traps that bite everyone, and the mutating-vs-non-mutating distinction that quietly causes UI bugs.


The Big Three

map โ€” transform every element, same length, new array:

const orders = [
{ id: 1, paise: 49900, status: "paid" },
{ id: 2, paise: 129900, status: "pending" },
{ id: 3, paise: 9900, status: "paid" },
];
const rupees = orders.map((o) => o.paise / 100); // [499, 1299, 99]
const labels = orders.map((o) => `#${o.id}: โ‚น${o.paise / 100}`);

The contract matters as much as the mechanics: map is one-in-one-out transformation. Using it for side effects while ignoring the result (orders.map(o => save(o)) with no use of the returned array) is a misuse the whole ecosystem frowns on โ€” thatโ€™s forEach or a loopโ€™s job. If you return nothing from the callback, youโ€™ve built an array of undefined.

filter โ€” keep matching elements, shorter-or-equal, new array:

const paid = orders.filter((o) => o.status === "paid");
const big = orders.filter((o) => o.paise > 20_000);

The predicate returns truthy/falsy โ€” which enables terse idioms like values.filter(Boolean) to strip null/undefined/empty (and, caution, also strips legitimate 0).

reduce โ€” boil the array down to one value:

const totalPaise = orders.reduce((sum, o) => sum + o.paise, 0);
// grouping โ€” the other canonical reduce
const byStatus = orders.reduce((acc, o) => {
(acc[o.status] ??= []).push(o);
return acc;
}, {});
// { paid: [...], pending: [...] }

Reduceโ€™s reputation for inscrutability is earned only when misused. The model: an accumulator starts at the seed (0, {}), the callback folds each element in, the final accumulator is the answer. Two disciplines keep it readable: always pass the seed (seedless reduce uses element one as the accumulator โ€” a type-mismatch trap and an exception on empty arrays), and if the callback exceeds ~3 lines, use a loop instead โ€” a for..of building the same accumulator is identical logic with better debuggability. Reduce is for when the fold is simple; itโ€™s not a badge of sophistication. (For grouping specifically, the new Object.groupBy(orders, o => o.status) replaces the reduce idiom where available.)

Chaining composes the three into the standard pipeline shape โ€” read it aloud like streams in Java:

const refundRupees = orders
.filter((o) => o.status === "paid")
.map((o) => o.paise / 100)
.reduce((a, b) => a + b, 0);

The Supporting Cast Youโ€™ll Use Weekly

// find / findIndex โ€” first match (or undefined / -1)
const order = orders.find((o) => o.id === 2);
const idx = orders.findIndex((o) => o.status === "pending");
// some / every โ€” boolean questions, short-circuiting
orders.some((o) => o.paise > 100_000); // "any big order?"
orders.every((o) => o.status === "paid"); // "all settled?"
// includes / indexOf โ€” for primitives (=== semantics; includes handles NaN)
["a", "b"].includes("b"); // true
// slice โ€” non-mutating subrange (half-open, like substring)
const firstTwo = orders.slice(0, 2);
const lastOne = orders.slice(-1);
// flat / flatMap โ€” nesting control
[[1, 2], [3]].flat(); // [1, 2, 3]
orders.flatMap((o) => o.items); // map + flatten one level: all items across orders
// at โ€” negative indexing, finally
orders.at(-1); // last element, no length arithmetic
// Array.from โ€” materialize iterables & generate sequences
Array.from({ length: 5 }, (_, i) => i * 10); // [0, 10, 20, 30, 40]

Small judgment notes: find returning undefined composes with optional chaining (orders.find(...)?.paise ?? 0); some/every beat filter(...).length > 0 (they stop early); and โ€œdoes this list of objects contain Xโ€ needs some with a predicate โ€” includes compares references, so structurally-equal objects wonโ€™t match.


Sort: Two Traps in One Method

// Trap 1: default sort is STRINGWISE โ€” even for numbers
[10, 9, 100].sort(); // [10, 100, 9] โ€” lexicographic!
[10, 9, 100].sort((a, b) => a - b); // [9, 10, 100] โ€” always pass a comparator
// objects: comparator by key, chained tiebreakers
orders.sort((a, b) => b.paise - a.paise); // descending by amount
users.sort((a, b) => a.city.localeCompare(b.city) || a.age - b.age);
// Trap 2: sort MUTATES the array it's called on โ€” and returns it
const sorted = orders.sort(...); // 'orders' itself is now reordered too!

The comparator contract: negative โ†’ a first, positive โ†’ b first, zero โ†’ tie. a - b ascending, b - a descending, localeCompare for human-correct strings. Get the contract wrong (returning booleans is the classic โ€” (a, b) => a > b returns true/false, i.e. 1/0, never negative) and you get subtly wrong orders that pass casual testing.

Trap 2 is the bridge to this guideโ€™s most important section.


Mutating vs. Non-Mutating: The Distinction That Causes UI Bugs

Array methods split into two families, and knowing which is which is load-bearing knowledge:

Non-mutating (returns new)Mutating (changes in place)
map, filter, slice, concat, flat, toSorted, toReversed, withpush, pop, shift, unshift, splice, sort, reverse, fill

Why it matters beyond hygiene: modern UI frameworks detect changes by reference comparison โ€” โ€œis this the same array object?โ€ Mutating an array keeps the same reference, so React/signal-based frameworks may conclude nothing changed and skip re-rendering. The bug presents as โ€œmy list updates in the data but not on screen,โ€ and its cause is a push or sort where a new array was needed.

ES2023 finally shipped non-mutating twins โ€” prefer them when you have them:

const sorted = items.toSorted((a, b) => a.paise - b.paise); // items untouched
const flipped = items.toReversed();
const patched = items.with(2, newItem); // copy with index 2 replaced

And the spread-based idioms that predate them remain everywhere:

const appended = [...items, newItem]; // non-mutating push
const removed = items.filter((x) => x.id !== id); // non-mutating delete
const updated = items.map((x) => x.id === id ? { ...x, qty } : x); // non-mutating update
const sortedOld = [...items].sort(cmp); // copy-then-sort, pre-toSorted

That update idiom โ€” map + conditional spread โ€” is the single most-typed pattern in React state code; the objects guide unpacks the { ...x, qty } half. The underlying discipline is one sentence: treat arrays you didnโ€™t just create as read-only; produce new ones. (Also remember const doesnโ€™t help here โ€” it freezes the binding, not the array.)


When a Plain Loop Is Still Right

The judgment section, because method-maximalism is its own disease:

The read-aloud test from the streams guide transfers verbatim: if the chain narrates cleanly (โ€œpaid orders, amounts, summedโ€), itโ€™s good; if narration needs breath marks, extract helpers or loop.


Three Real Tasks, Composed

Method fluency is combinatorial โ€” the payoff is composing them under realistic requirements. Work each before reading the solution.

Task 1: Top 3 customers by total spend (from a flat orders array with customer and paise):

const top3 = Object.entries(
orders.reduce((acc, o) => {
acc[o.customer] = (acc[o.customer] ?? 0) + o.paise;
return acc;
}, {})
)
.toSorted(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([customer, paise]) => `${customer}: โ‚น${paise / 100}`);

The shape to recognize: aggregate into an object, then Object.entries back into array-land for sorting and slicing โ€” the same two-stage pipeline as Java streamsโ€™ โ€œtop Nโ€, with destructuring doing the pair-handling.

Task 2: Are two arrays equal by contents? No built-in โ€” and now you can write the idiom: a.length === b.length && a.every((x, i) => x === b[i]). Note everyโ€™s rarely-used second parameter (the index) doing the pairwise walk.

Task 3: Paginate. items.slice((page - 1) * size, page * size) โ€” half-open ranges making the arithmetic clean, and a reminder that not everything needs a callback method; slice alone is the whole feature.

Frequently Asked Questions

forEach vs for..of? for..of is the more capable default โ€” supports break/continue/await and reads as what it is. forEach is fine for genuinely simple per-element side effects at a chainโ€™s end. Never for async.

How do I get unique values? Primitives: [...new Set(values)] โ€” idiomatic and fast. Objects by key: a Map keyed on the identity field โ€” [...new Map(items.map(i => [i.id, i])).values()] (keeps the last occurrence per id; a seen-Set + filter keeps the first).

Why did map(parseInt) mangle my numbers? ["10","10","10"].map(parseInt) โ†’ [10, NaN, 2] โ€” map passes (element, index, array), and parseInt interprets the index as a radix. The fix: map(Number) or map(s => parseInt(s, 10)). General lesson: be careful passing multi-parameter functions directly as callbacks.

Are these methods slower than loops? Marginally, per-call โ€” and irrelevantly for typical sizes; engines optimize them hard. Choose by clarity; optimize the rare proven hot spot by fusing into a loop.

Empty-array edge cases? map/filter โ†’ []; reduce with seed โ†’ the seed; some โ†’ false; every โ†’ true (vacuous truth โ€” surprising but logical, and occasionally a bug when โ€œeveryโ€ of nothing shouldnโ€™t count as success).

Why does my array have holes, and do methods skip them? Sparse arrays (new Array(3), deleted indexes, trailing commas gone wrong) have missing slots, and classic methods (map, forEach) skip holes while newer ones (fill, spread, Array.from) treat them as undefined โ€” a consistency mess best avoided by never creating sparse arrays. Array.from({ length: n }, fn) is the hole-free way to generate.


Iterator Helpers: The Next Chapter Arriving Now

A recent addition worth knowing as it lands everywhere: iterator helpers put map/filter/take directly on iterators, evaluated lazily โ€” no intermediate arrays:

const firstThreeBig = orders.values()
.filter((o) => o.paise > 50_000)
.map((o) => o.id)
.take(3)
.toArray();

Unlike array-method chains (each step materializes a full array), iterator chains pull one element through the whole pipeline at a time and stop when take(3) is satisfied โ€” the same lazy, short-circuiting model as Java streams, arriving in JS twenty years of ecosystem later. For typical small arrays the difference is academic; for large data, generated sequences, and early-exit searches, itโ€™s the tool the language was missing. The mental model transfers wholesale from this guide โ€” same verbs, lazy engine.

Self-Check

const xs = [3, 1, 2];
// 1
const ys = xs.sort();
console.log(xs, ys === xs);
// 2
console.log([1, 2, 3].map((n) => { n * 2 }));
// 3
console.log([[1], [2, 3]].flatMap((a) => a.map((n) => n * 10)));
// 4 โ€” what's the bug pattern?
const total = items.reduce((sum, i) => sum + i.paise);
// 5
console.log([5, 25, 125].filter(Boolean).every((n) => n % 5 === 0));

Answers: (1) [1,2,3], true โ€” sort mutated xs and returned the same reference (and got lucky: single digits sort โ€œcorrectlyโ€ stringwise โ€” a trap that hides until 10 shows up). (2) [undefined, undefined, undefined] โ€” braces without return, the arrow-body trap. (3) [10, 20, 30]. (4) missing seed โ€” on empty items it throws; with items, the first object becomes the accumulator and sum + i.paise becomes string/NaN nonsense. Always seed. (5) true.

Next: the other half of daily data wrangling โ€” Objects, Destructuring, and Spread: shorthand, computed keys, immutable updates, deep-vs-shallow copies, and Map/Setโ€™s proper place.