Sorting & copying safely
Sort arrays with reliable comparators, avoid accidental mutation, and use modern copying methods like toSorted, toReversed, toSpliced, and with.
- 01Write reliable comparatorsSort numbers, strings, and objects without default-sort surprises.
- 02Choose mutation or copyingKnow when sort, reverse, splice, and index assignment change the original array.
- 03Use ES2023 copy methodsReach for toSorted, toReversed, toSpliced, and with when shared data must stay safe.
Sort without surprises
Arrays often arrive in the wrong order: newest messages first, product prices mixed together, or names from a form exactly as people typed them. JavaScript gives you powerful tools to reorder those lists, but two details matter every time: what rule decides the order and whether the original array changes.
This lesson sits after Array basics, Slicing, splicing & joining, Searching arrays, and map, filter & reduce. It also leans on Comparisons for the idea that strings compare by UTF-16 code unit, so "Zebra" < "apple" and "10" < "9". From Objects & references, remember that mutation is visible through every reference to the same array. Arrow functions give us compact comparators such as (a, b) => a - b.
Use a comparator when the default string ordering is not exactly what you want. Use copying methods when another part of your app may still need the original order.
Imagine a volunteer alphabetizing cards by the characters printed on them. The card “100” goes near “10”, not near “99”, because the first character decides early. That is exactly the surprise behind default array sorting.
- In real life: Cards are compared letter by letter
- In JavaScript: Default
sort()compares string forms - In real life: Card 100 files before card 9 because it starts with 1
- In JavaScript:
[10, 9, 1, 100].sort()places 100 before 9 - In real life: A librarian can be given a different filing rule
- In JavaScript: A comparator supplies the rule JavaScript should use
Where the analogy stops: Library cards are always text. JavaScript arrays can hold numbers, objects, undefined values, and holes, so professional code should state the intended rule instead of hoping the default matches.
We will move from the default rule, to numeric and object comparators, to mutation, to the ES2023 copying methods: toSorted, toReversed, toSpliced, and with. Then we will use stable sorting to build multi-step sorts without losing tie order.
sort & comparators
STEP THROUGHarray.sort() sorts the array in place. If you do not pass a comparator, JavaScript converts the defined elements to strings and compares those strings. That is useful for a quick alphabetical list of simple ASCII-ish labels, but it is a trap for numbers.
Step through the default-sort surprise. Then switch to a numeric comparator and to one verified localeCompare example. Locale-aware sorting gets its own lesson next module, Comparing & sorting text; here we only preview the API so you know default sort is not the right tool for human language.
Predict the printed order, then step through the real run. Change the comparator and predict again.
script
const sorted = numbers.sort();console.log(sorted.join(", "));A sorting algorithm does not know what “best” means for your data. It asks your comparator to judge two items at a time. The comparator does not move items itself; it only answers who should come first.
- In real life: The referee compares two players
- In JavaScript: The comparator receives two items,
aandb - In real life: Negative means player A goes first
- In JavaScript: Return a negative number to put
abeforeb - In real life: Positive means player B goes first
- In JavaScript: Return a positive number to put
bbeforea - In real life: A tie keeps the queue calm
- In JavaScript: Return 0 when their order should be unchanged
Where the analogy stops: The engine may ask the referee about pairs in any order, and more than once. Do not write comparators with side effects that depend on the exact call sequence.
For numbers, the common ascending rule is (a, b) => a - b. If a is smaller, the subtraction is negative, so a goes first. For descending order, flip it to (a, b) => b - a. For objects, compare the property you care about, and use || for tie-breakers.
const sorted = [...people].sort((a, b) => { return a.age - b.age;});- Noah Patel, age 20
- Leo Patel, age 20
- Zoe Kim, age 25
- Mia Stone, age 30
- Ava Stone, age 30
Comparator-call log will appear after this runs in your browser.
The playground uses [...people].sort(...), so the original data is copied first. Comparator-call details appear after the browser mounts.
You may see (a, b) => a > b. It returns true or false, which become 1 and 0, but it never returns a negative number. It may appear to work in one small test and fail in another order. Return a number: negative, positive, or zero.
reverse and mutation
STEP THROUGHreverse() is simpler than sort(): it flips the current order. But it shares the important danger: it mutates the original array and returns the same array object. That is fine for a local scratch array. It is dangerous for shared data, props, state, caches, or anything another reference might read later.
Step through the mutation demo. The important question is not only what prints, but whether a new array was created.
script
const sortedReturn = numbers.sort((a, b) => a - b);console.log(numbers.join(", "));console.log(numbers === sortedReturn);const reversedReturn = numbers.reverse();console.log(numbers.join(", "));console.log(numbers === reversedReturn);The shared-reference bug looks like this: one variable is named visibleProducts, another is named allProducts, but both point at the same array. Sorting the visible list in place silently rearranges the master list too. You learned in Objects & references that mutation is visible through every reference. Arrays follow the same rule because arrays are objects.
const prices = [30, 10, 20];const lowToHigh = [...prices].sort((a, b) => a - b);const highToLow = prices.slice().sort((a, b) => b - a); console.log(prices); // [30, 10, 20]console.log(lowToHigh); // [10, 20, 30]Spread and slice() make shallow copies. Then sort() or reverse() can mutate the copy without touching the original array order. The objects inside are still shared; only the list container is copied.
toSorted, toReversed, toSpliced & with()
INTERACTIVEModern JavaScript gives copying versions of the mutation-heavy array operations. Think of sort() as rearranging the original bookshelf. toSorted() photographs the shelf and rearranges the photo. The shelf itself is unchanged.
| Change you want | Mutates original | Returns a changed copy |
|---|---|---|
| Sort items | sort(compare) | toSorted(compare) |
| Reverse items | reverse() | toReversed() |
| Remove or insert | splice(start, deleteCount, ...items) | toSpliced(start, deleteCount, ...items) |
| Replace one index | array[index] = value | with(index, value) |
const letters = ["A", "B", "C", "D"];console.log(letters.toReversed().join(""));console.log(letters.toSpliced(1, 2, "X").join(""));console.log(letters.with(1, "Z").join(""));console.log(letters.join(""));Checking this browser's Array.prototype support…
Feature detection runs after the browser mounts, avoiding server/browser mismatches.
with(index, value) deserves special attention because it replaces the common mutation arr[i] = value. It accepts negative indexes like at(): arr.with(-1, "done") replaces the last item. If the index is outside the array, it throws a RangeError instead of silently creating a sparse array.
array.sort(compare)array.reverse()array.splice(1, 2)array[1] = valuearray.toSorted(compare)array.toReversed()array.toSpliced(1, 2)array.with(1, value)
Sort each array operation by whether it changes the original array or returns a changed copy.
These copying methods are ES2023 features and are broadly supported in modern browsers. If you support older browsers, use feature detection, transpilation, or the spread-and-sort pattern until your baseline includes them.
Stable sorting
GUARANTEEDA stable sort keeps tied items in the order they arrived. Picture a queue: if Mia and Ava both have age 30, a stable age sort does not randomly swap them. JavaScript array sorting is stable in modern JavaScript (the guarantee has been part of the language since ES2019).
const queue = [ { name: "Mia", age: 30 }, { name: "Noah", age: 20 }, { name: "Ava", age: 30 }, { name: "Leo", age: 20 },];const byAge = queue.toSorted((a, b) => a.age - b.age);console.log(byAge.map((person) => person.name).join(", "));The output is Noah, Leo, Mia, Ava. Noah and Leo both have age 20, and Noah arrived first, so he stays before Leo. Mia and Ava both have age 30, and Mia stays before Ava. Stability lets you sort by a secondary key first, then by a primary key, though a single comparator with an explicit tie-breaker is often clearer.
At a help desk, people with the same priority keep the order they arrived in. Stability gives your equal items that same fairness.
- In real life: People with the same ticket priority keep arrival order
- In JavaScript: Items where comparator returns 0 keep relative order
- In real life: Changing priority can move someone
- In JavaScript: A nonzero comparator result can reorder items
- In real life: The queue rule is predictable
- In JavaScript: Stable sort makes tie behavior safe to rely on
Where the analogy stops: A queue has one obvious next person. A sorting algorithm may compare many pairs internally, so stability only describes the final order of ties, not the internal steps.
Where you'll use this
Sorting and safe copying show up anywhere UI data is displayed differently from how it is stored. A product page may keep server results untouched, then show price low-to-high. A table may let users click Name, Age, or Joined date without mutating the fetched data. A reducer may replace one cart item without changing the previous state object.
- Sortable tables
- Leaderboards
- Search results
- Shopping carts
- Undo/redo state
- Grouped reports
function sortRows(rows, sortKey) { return rows.toSorted((a, b) => { if (sortKey === "name") return a.name.localeCompare(b.name); if (sortKey === "score") return b.score - a.score; return 0; });}Notice the function returns a new array every time. That makes it friendly for UI state, testing, and undo history.
Common misconceptions
“sort knows numbers are numbers.”
Not by default. Without a comparator, defined values are sorted by string form, so 100 comes before 9.
“Returning true or false is a comparator.”
A comparator must return a number with negative, positive, and zero meanings. A boolean loses the negative case and is unreliable.
“sort and reverse give me a new array.”
They return the same array they mutate. Use toSorted, toReversed, or copy first.
“Stable means non-mutating.”
Stable only means tied items keep their relative order. sort is stable and mutating; toSorted is stable and copying.
“undefined sorts like the word undefined.”
Array sort places undefined elements after defined elements. If you log with join, they appear as empty fields.
Practice: sorting safely
5 EXERCISESRun the code and check the printed result. Then try replacing toSorted with sort and explain what changes.
const scores = [10, 9, 1, 100];
console.log(scores.toSorted((a, b) => a - b).join(", "));const scores = [10, 9, 1, 100];
console.log(scores.toSorted((a, b) => a - b).join(", "));toSorted keeps scores unchanged and the comparator makes the order numeric, so the printout is 1, 9, 10, 100.
Use a tie-breaker comparator to sort full names by last name, then first name.
const people = ["Grace Hopper", "Ada Lovelace", "Grace Murray", "Ada Byron"];
const sorted = people.toSorted((a, b) => {
const [aFirst, aLast] = a.split(" ");
const [bFirst, bLast] = b.split(" ");
return aLast.localeCompare(bLast) || aFirst.localeCompare(bFirst);
});
console.log(sorted.join(" | "));const people = ["Grace Hopper", "Ada Lovelace", "Grace Murray", "Ada Byron"];
const sorted = people.toSorted((a, b) => {
const [aFirst, aLast] = a.split(" ");
const [bFirst, bLast] = b.split(" ");
return aLast.localeCompare(bLast) || aFirst.localeCompare(bFirst);
});
console.log(sorted.join(" | "));The comparator returns the last-name comparison unless it is 0. Then || falls through to the first-name comparison.
A shared list must stay unchanged. Use a copying method so the first log still shows the original order.
const original = ["draft", "sent", "archived"];
const sorted = original.toSorted();
console.log(original.join(", "));
console.log(sorted.join(", "));const original = ["draft", "sent", "archived"];
const sorted = original.toSorted();
console.log(original.join(", "));
console.log(sorted.join(", "));The first log proves the original array still has its original order. The sorted copy can be displayed somewhere else.
Replace sugar with honey without mutating the original cart.
const cart = ["tea", "sugar", "milk"];
const nextCart = cart.with(1, "honey");
console.log(nextCart.join(", "));
console.log(cart.join(", "));const cart = ["tea", "sugar", "milk"];
const nextCart = cart.with(1, "honey");
console.log(nextCart.join(", "));
console.log(cart.join(", "));with replaces only index 1 in the returned copy. The original cart still contains sugar.
Before running it, predict exactly what the default sort prints.
console.log([10, 9, 1].sort().join(", "));console.log([10, 9, 1].sort().join(", "));The string order is 1, 10, 9, so the joined output is 1, 10, 9.
Quiz: check your understanding
7 QUESTIONSRead each explanation, especially when your first guess is wrong. Sorting mistakes often look correct on tiny samples.
Question 1 of 7What does default sorting numbers print?
Read the code, then predictconsole.log([10, 9, 1, 100].sort().join(", "));Choose an answer to see the explanation.
Question 2 of 7Which comparator sorts numbers ascending?
Choose an answer to see the explanation.
Question 3 of 7What does identity after sort print?
Read the code, then predictconst a = [3, 1, 2]; const b = a.sort((x, y) => x - y); console.log(a === b);Choose an answer to see the explanation.
Question 4 of 7Which method returns a changed copy with one item replaced?
Choose an answer to see the explanation.
Question 5 of 7What do toReversed logs print?
Read the code, then predictconst names = ["Mia", "Noah", "Ava"]; console.log(names.toReversed().join(", ")); console.log(names.join(", "));Choose an answer to see the explanation.
Question 6 of 7What does stable sorting guarantee?
Choose an answer to see the explanation.
Question 7 of 7What happens here?
Read the code, then predictconst values = [3, undefined, 1]; values.sort(); console.log(values.join("|"));Choose an answer to see the explanation.
Key takeaways
- Default
sort()compares string forms, so numeric arrays need comparators. - A comparator returns negative for
afirst, positive forbfirst, and zero for a tie. sort,reverse,splice, and index assignment mutate the original array.toSorted,toReversed,toSpliced, andwithreturn changed copies.- JavaScript sorting is stable, so tied items keep their relative order.
Remember the one-liner.
Sort with an explicit rule, and copy before you rearrange shared data.
Up next: Iterating arrays & array-likes.