The short version: in 2026 you have negative indexing with at(), recursive flattening with flat(Infinity), a full family of copying methods (toReversed, toSorted, toSpliced, with) that finally let you reorder an array without mutating it, and grouping with Object.groupBy / Map.groupBy. That last one matters because a lot of older posts (mine included, once) tell you to use Array.prototype.group, which was renamed and never shipped. Here is what is real now, with the browser and Node floor for each.
at() for negative indexing
To read the last element of an array you used to write arr[arr.length - 1]. It works, but it is noisy and easy to fat-finger. at() takes a negative index and counts from the end:
const parts = ["a", "b", "c"];
parts.at(-1); // "c"
parts.at(-2); // "b"
parts[parts.length - 1]; // "c", the old wayBracket access does not understand negative numbers (arr[-1] is just a property lookup that returns undefined), which is exactly the trap at() removes. It also works on strings and typed arrays, so "hello".at(-1) is "o". at() has been Baseline since 2022 (Chrome 92, Firefox 90, Safari 15.4, Node 16.6), so on any current runtime you can reach for it without a second thought.
flat() and flatMap()
flat() flattens nested arrays. The default depth is 1, so it only unwraps one level. For an arbitrarily deep structure, pass Infinity:
[1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]] (depth 1)
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]That flat(Infinity) line is the whole answer to "how do I recursively flatten a nested array in JavaScript." It supersedes the hand-rolled recursive helpers and library flatten functions that older code carried around.
flatMap() maps each element and then flattens the result by exactly one level. It is the idiomatic way to expand-or-drop items in one pass: return an array of several items to expand, or an empty array to drop:
const te_words = (s) => s.split(" ");
["the lazy dog", "fox"].flatMap(te_words);
// ["the", "lazy", "dog", "fox"]
// Drop odd numbers, double the even ones, in one pass:
[1, 2, 3, 4].flatMap((n) => (n % 2 ? [] : [n * 2]));
// [4, 8]Both have been Baseline since 2020 (Node 11+), so they are safe everywhere.
The mutation trap: copying methods vs in-place methods
This is the part that bites people, so it gets the most space. reverse(), sort(), and splice() all mutate the array in place and return a reference to it (or, for splice, the removed elements). The return value lulls you into thinking you got a fresh array. You did not:
const scores = [3, 1, 2];
const reversed = scores.reverse();
// reversed is [2, 1, 3] ... and so is scores, now also [2, 1, 3].
// You just mutated the original while trying to make a copy.If scores came in as a function argument, a prop, or a value someone else still holds a reference to, you have introduced a bug that shows up far from where you wrote it. The same trap applies to sort() (which also mutates) and splice().
The old fix was to copy first, then mutate the copy:
const reversed = [...scores].reverse(); // scores untouched
const sorted = [...scores].sort((a, b) => a - b);Since ES2023 there is a cleaner fix: a copying counterpart for each mutating method. toReversed(), toSorted(), toSpliced(), and with() all leave the original alone and return a new array:
const scores = [3, 1, 2];
const reversed = scores.toReversed(); // [2, 1, 3]
const sorted = scores.toSorted((a, b) => a - b); // [1, 2, 3]
const patched = scores.with(0, 99); // [99, 1, 2] (replace index 0)
const spliced = scores.toSpliced(1, 1); // [3, 2] (remove 1 at index 1)
scores; // [3, 1, 2] throughout. Nothing above touched it.with(index, value) is the copying replacement for arr[index] = value: it returns a new array with one element swapped, which is exactly what you want in a React setState or any immutable update. toSpliced(start, deleteCount, ...items) mirrors splice() for inserting and removing.
The trade-off is honest: the copying methods allocate a new array every call, so in a tight loop over a huge array the in-place version is cheaper. For ordinary application code the readability and the absence of spooky-action-at-a-distance bugs win easily.
These four are ES2023, Baseline since 2023: Chrome 110, Firefox 115, Safari 16, Node 20+. If you ship to runtimes older than that (Node 18, old Safari), keep using the spread-then-mutate form or a polyfill.
Grouping: Object.groupBy and Map.groupBy (not Array.prototype.group)
Here is the correction. For years the grouping proposal was written up as Array.prototype.group and Array.prototype.groupToMap. Those names were withdrawn: a popular library had already monkey-patched Array.prototype with an incompatible group, so TC39 moved the feature off the prototype and onto static methods. Array.prototype.group and groupToMap never shipped to any stable browser. If a tutorial tells you to call arr.group(...), it is teaching an API that does not exist.
The real API is Object.groupBy(items, callbackFn):
const teams = [
{ name: "Arsenal", tier: "top" },
{ name: "Spurs", tier: "mid" },
{ name: "City", tier: "top" },
];
const byTier = Object.groupBy(teams, ({ tier }) => tier);
// {
// top: [{ name: "Arsenal", ... }, { name: "City", ... }],
// mid: [{ name: "Spurs", ... }],
// }The callback returns the group key for each element; Object.groupBy collects the elements into arrays under those keys. One useful detail: the object it returns has a null prototype (Object.create(null)), so there is no risk of a group key like "constructor" or "__proto__" colliding with inherited properties.
Use Map.groupBy when your group key is not a string (an object, a number you want to keep as a number, anything). It returns a Map, and Map keys can be any value:
const odd = { label: "odd" };
const even = { label: "even" };
const byParity = Map.groupBy([1, 2, 3, 4], (n) => (n % 2 ? odd : even));
byParity.get(odd); // [1, 3]
byParity.get(even); // [2, 4](Object.groupBy would coerce those object keys to the string "[object Object]" and merge them, which is why Map.groupBy exists.) Note that both are static methods. You call Object.groupBy(arr, fn), never arr.groupBy(fn).
Support is the newest thing on this page: Baseline since March 2024, Chrome/Edge 117, Firefox 119, Safari 17.4, Node 21+. On older runtimes the long-standing reduce-into-an-object pattern still does the job (see the reduce guide for that fold), and grouping is now the textbook case where Object.groupBy replaces a reduce you used to write by hand.
Support at a glance
| Method | First Baseline | Node floor | Mutates? |
|---|---|---|---|
at() | 2022 | 16.6 | No |
flat() / flatMap() | 2020 | 11 | No |
toReversed() / toSorted() / toSpliced() / with() | 2023 | 20 | No (copying) |
reverse() / sort() / splice() | always | any | Yes (in place) |
Object.groupBy() / Map.groupBy() | 2024 | 21 | No |
See also
- Array.prototype.reduce in JavaScript: the fold these methods sit alongside, and the grouping pattern
Object.groupBynow replaces. - Remove duplicate values from an array:
[...new Set(arr)]for primitives, and how to dedupe objects by key. - Merge arrays in JavaScript: spread,
concat, andflatfor combining arrays. - Destructuring in JavaScript: the syntax behind the
({ tier }) => tiercallbacks above. - Promises and async/await: when array work needs to happen asynchronously, the patterns for mapping over async work.
Sources
Authoritative references this article was fact-checked against.
- Array.prototype.at() (MDN Web Docs)developer.mozilla.org
- Array.prototype.flat() (MDN Web Docs)developer.mozilla.org
- Array.prototype.toReversed() (MDN Web Docs)developer.mozilla.org
- Array.prototype.toSorted() (MDN Web Docs)developer.mozilla.org
- Object.groupBy() (MDN Web Docs)developer.mozilla.org
- Map.groupBy() (MDN Web Docs)developer.mozilla.org





