JavaScript Modern Array Methods Table

MethodWhat it doesField note
toSorted(cmp)sorted() but NEW arrayES2023 - sort() no longer must mutate; React state solved
toReversed()reversed() but NEW arraySame copy-first contract for reverse
toSpliced(start, n, ...)spliced() but NEW arrayInsert/remove without touching the original
with(i, value)Replace one elementNEW array with index i swapped - immutable updates in one call
Object.groupBy(items, fn)Group to objectES2024 - group by key; keys become STRINGS
Map.groupBy(items, fn)Group to MapKey can be ANYTHING - objects, DOM nodes, real types
at(-1)Negative indexinglast element without arr[arr.length-1] gymnastics
findLast / findLastIndexSearch from the endThe recent-item lookup without reverse().find()
Reference: the MDN toSorted reference. ES2023/2024 closed array's oldest design debt: half the methods MUTATED (sort, reverse, splice) while half returned new arrays (map, filter, slice) - and immutable-style code had to hand-spread around the mutators. toSorted, toReversed, toSpliced and with complete the copy-first set; Object.groupBy and Map.groupBy kill the reduce-accumulator group-by idiom. Bottom line: with() is the one-call immutable update, at(-1) ends the length-minus-one gymnastics, and the groupBy pair differ in KEY TYPE - object keys are strings, Map keys are anything. Related tools: structuredClone table (the other immutable-style tool), array methods table (the classics these extend), and map-set table (why Map keys change groupBy's game).

ES2023 and ES2024 closed array's oldest design debt: half the methods MUTATED (sort, reverse, splice) while half returned new arrays (map, filter, slice), and immutable-style code had to hand-spread around every mutator. toSorted, toReversed, toSpliced and with complete the copy-first set.

Bottom line: with(index, value) is the one-call immutable update (a new array with one element swapped), at(-1) ends the length-minus-one gymnastics, and the groupBy pair differ in KEY TYPE - Object.groupBy coerces keys to strings, Map.groupBy accepts anything including objects and DOM nodes.

The honest part: these methods exist because React-era state management made array immutability a daily operation - the language finally ships the verbs the framework patterns assumed. On a codebase with zero immutable-state needs, they are convenience, not revolution.

How to use

  1. Update state immutably in one call: setItems(items.with(editedIndex, newItem)) - no spread-of-slice sandwich for a single element change.
  2. Sort without mutating: const ranked = players.toSorted((a, b) => b.score - a.score) - the original stays pristine for the next re-render.
  3. Group by computed key: const byDept = Object.groupBy(employees, e => e.dept) - or Map.groupBy when the key is an object, node or real number.

Frequently asked questions

Why did toSorted/toReversed/toSpliced take twenty-five years to arrive?

Because sort(), reverse() and splice() predate the immutability era - they were designed as in-place commands in 1997, and fixing their behavior would break every existing program that relies on them returning the SAME array (mutated). The TC39 compromise is the copy-first siblings: identical signatures, new-array returns, leaving the originals untouched. The naming (to-prefix) marks the contract at a glance. The practical payoff shows in framework state: items.sort() inside a React setState was the classic silent bug - the state array mutated in place, references stayed equal, and re-renders skipped - while toSorted makes the intent and the new reference explicit.

What is the real difference between Object.groupBy and Map.groupBy?

Key type coercion. Object keys are strings (or symbols): Object.groupBy(items, p => p.age) gives age keys as strings, and object keys like DOM nodes or mixed-type values collapse ('1' and 1 are the same key). Map.groupBy uses same-value-zero semantics: real numbers stay numbers, objects and elements work as keys, and iteration order is insertion order guaranteed. The rule: string-shaped categories (department names, status labels) fit Object.groupBy's convenient dot access; anything else - numeric buckets, object references, DOM elements - needs Map.groupBy, whose values read back with .get(key) instead of property access.

How does with() change immutable update code?

It collapses the update idiom. Replacing one element immutably used to be items.map((x, i) => i === idx ? newItem : x) or the spread-slice sandwich [...items.slice(0, idx), newItem, ...items.slice(idx + 1)] - both noisy, both easy to get wrong at boundaries. items.with(idx, newItem) states the intent in one call, handles negative indices (with(-1, x) replaces the last), and returns a new array with every other element shared (cheap - no copying of element contents). The same ergonomics apply to toSpliced for insert/remove: the immutable splice finally reads like the mutating one it mirrors.

When do at() and findLast genuinely beat the older idioms?

at() wins wherever negative indexing reads naturally: arr.at(-1) for last, arr.at(-2) for second-to-last - against arr[arr.length - 1], it is the difference between intent and arithmetic, and it works on strings and typed arrays too. findLast wins when the newest match matters: findLast(e => e.status === 'error') scans from the end - the log-tail and latest-event case - without the reverse() copy or the manual backwards loop. Neither enables anything new; both remove the small gymnastics that added up across a codebase. For hot inner loops over millions of items, classic indexed access remains fastest - these are ergonomics methods, and they cost roughly what they save.

Related tools