Sorting & copying safely
Sort arrays with reliable comparators, avoid accidental mutation, and use modern copying methods like toSorted, toReversed, toSpliced, and with.
- 01Write comparators that workSort numbers, words, and objects with your own rule, so 100 never lands before 9 by surprise.
- 02Know which methods change the arrayTell when sort, reverse, splice, and array[index] = value change the original array.
- 03Sort a copy insteadUse toSorted, toReversed, toSpliced, and with when other code still needs the original array.
Put a list in order
Lists often come in the wrong order. Messages may come newest first. Prices may be mixed up. Names from a form come in the order people typed them.
To sort a list means to put its items in order. JavaScript arrays have methods that sort. Each time you sort, ask two questions:
- What rule decides the order? For example, smallest number first.
- Should the original array change? Or should you get a new, sorted array and keep the original as it was?
This lesson uses three things from earlier lessons:
- From Comparisons: text is compared one character at a time, using each character's code. Capital letters come before small letters, so
"Zebra" < "apple"istrue. And"10" < "9"istruetoo, because 1 comes before 9. - From Objects & references: two variables can point at the same array. A change made through one variable shows up through the other.
- From Arrow functions: short functions such as
(a, b) => a - b.
Give the sort your own rule when the default order is not what you want. Sort a copy when other code still needs the original order.
const prices = [30, 10, 20];
const sortedPrices = prices.toSorted((a, b) => a - b);
console.log(sortedPrices.join(", "));Line 1 makes a short list of prices. Line 2 calls toSorted. It makes a new array, sorts it, and returns it. The original array is not touched. The function (a, b) => a - b is the rule: smaller numbers first. Line 3 prints 10, 20, 30. The prices array still holds 30, 10, 20.
Picture a helper who files library cards by the characters printed on them. They look at the first character first. The card "100" starts with 1, so it goes next to "10", far away from "99".
- In real life: Cards are compared one character at a time, from the left
- In JavaScript: Default
sort()turns items into text and compares the text - In real life: Card 100 goes before card 9, because 1 comes before 9
- In JavaScript:
[10, 9, 1, 100].sort()puts 100 before 9 - In real life: You can give the librarian a different filing rule
- In JavaScript: A comparator gives
sorta different rule
The lesson goes in this order:
- The default sort rule, and why it surprises people with numbers.
- Your own rules, for numbers and for objects.
- Methods that change the original array:
sortandreverse. - Newer methods that return a changed copy instead:
toSorted,toReversed,toSpliced, andwith. - Stable sorting, which keeps tied items in their first order.
sort() and comparators
STEP THROUGHarray.sort() puts the items of an array in order. It changes the original array: the items move around inside it.
If you call sort() with nothing in the brackets, it uses the default rule:
- Each item is turned into text. The number
100becomes the text"100". - The texts are compared one character at a time, from the left.
undefineditems are not turned into text. They always go to the end.
This rule works for simple words. For numbers it is a trap: the text "100" starts with 1, so it comes before "9".
sort() turns each number into text and compares the text one character at a time. "100" starts with 1, and 1 comes before 9, so 100 lands before 9.Step through the code below to see this happen. Then pick another rule: smallest number first, largest number first, or one example with localeCompare.
localeCompare compares words the way people expect in a language, such as English. It has its own lesson in the next module, Comparing & sorting text. For now, remember one thing: for words that people read, the default rule is not the right tool.
Guess the printed order, then step through the real run. Pick another rule and guess again.
script
const sorted = numbers.sort();console.log(sorted.join(", "));To use your own rule, pass a function to sort. This function is called a comparator. sort calls it with two items, a and b. The comparator returns a number:
- A negative number (below 0) means
agoes first. - A positive number (above 0) means
bgoes first. 0means a tie, so the two keep their order.
a first. A positive number puts b first. 0 means a tie, so the two keep their order.The sort method does not know what order you want. So it asks a referee. The referee looks at two players at a time and answers one question: who goes first? The referee never moves the players. It only gives the answer.
- In real life: The referee looks at two players
- In JavaScript: The comparator gets two items,
aandb - In real life: "Player A first"
- In JavaScript: Return a negative number to put
abeforeb - In real life: "Player B first"
- In JavaScript: Return a positive number to put
bbeforea - In real life: "It's a tie, stay where you are"
- In JavaScript: Return 0 to keep their order
For numbers, the usual rule for smallest first is (a, b) => a - b. If a is smaller, a - b is negative, so a goes first. For largest first, swap the two: (a, b) => b - a.
For objects, compare the property you care about, such as a.age - b.age. To break a tie, add a second comparison after ||. The second comparison is used only when the first one gives 0. You can try this in the playground below.
const prices = [3, 1, 2];
const sorted = prices.toSorted((a, b) => a - b);
console.log(sorted.join(", "));
console.log(prices.join(", "));A comparator must be consistent. That means the same two items must always give the same answer, every time sort asks. So inside a comparator, don't:
- read values that can change while the sort runs,
- use random numbers,
- change the array.
This code prints 1, 2, 3 and then 3, 1, 2. toSorted sorted a copy, so prices kept its first order.
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
The calls appear once the page has loaded in your browser.
The code copies the array first with [...people], so the original list does not change. The comparator calls appear once the page has loaded in your browser.
You may see (a, b) => a > b. It returns true or false. sort turns those into 1 and 0. So it never returns a negative number, and sort never hears "a goes first".
It may look right in one small test and then fail with a different list. Always return a number: negative, positive, or zero.
reverse() changes the original too
STEP THROUGHreverse() flips the order of an array: the last item becomes the first. It is simpler than sort(), but it has the same risk:
- It changes the original array.
- It returns that same array, not a new one.
Changing the original array is called mutating it. That is fine for an array that only one small piece of code uses. It is risky for an array that other code also reads later, such as a list of products shared across your app.
Step through the code. Watch two things: what prints, and whether a new array was made.
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);Here is the bug this causes. Say you have two variables, visibleProducts and allProducts. Both point at the same array. You sort visibleProducts with sort(). Now allProducts is in the new order too, with no warning.
You saw this rule in Objects & references: a change made through one variable shows up through every variable that points at the same object. Arrays are objects, so they follow the same rule.
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]Line 2 copies the array with spread, [...prices]. Line 3 copies it with slice(). Each line then sorts its own copy, so prices keeps its order.
These are shallow copies. You get a new array, but the items inside are not copied. If the items are objects, both arrays hold the same objects.
const rows = [{ product: "Tea", price: 3 }];
const copy = [...rows];
copy[0].price = 4;
console.log(rows[0].price);
console.log(rows === copy);Line 1 makes an array with one object in it, a row. Line 2 copies the array. Line 3 changes the price through the copy. The output is 4 and then false:
4: the original row changed too, because both arrays hold the same row object.false: the two arrays themselves are different arrays.
If you need to change the objects inside, copy those objects too.
toSorted, toReversed, toSpliced & with()
INTERACTIVENewer JavaScript has a copying partner for each method that changes the array. The copying partner returns a new, changed array. The original array stays as it was.
For example, sort() moves the items inside the original array. toSorted() makes a new array, sorts that, and returns it.
sort() moves the items inside the original array, so prices changes. toSorted() leaves prices as it was and gives you a new, sorted array.Each row of the table below is one change you might want. The middle column changes the original array. The last column gives you a changed copy instead.
| The change you want | Changes the original | Returns a changed copy |
|---|---|---|
| Sort the items. | sort(compare) | toSorted(compare) |
| Reverse the items. | reverse() | toReversed() |
| Remove or insert items. | splice(start, deleteCount, ...items) | toSpliced(start, deleteCount, ...items) |
| Replace the item at 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 which methods this browser has…
The check runs once the page has loaded in your browser.
with(index, value) is the copying partner of arr[i] = value. It returns a copy where the item at index is now value.
- A negative index counts from the end, as it does with
at(). Soarr.with(-1, "done")replaces the last item. - An index outside the array throws a
RangeError, so your code stops with an error right away. Writingarr[i] = valuethere would instead leave empty slots in the array, with no warning.
array.sort(compare)array.reverse()array.splice(1, 2)array[1] = valuearray.toSorted(compare)array.toReversed()array.toSpliced(1, 2)array.with(1, value)
Put each line into a group: does it change the original array, or return a changed copy?
These copying methods were added to JavaScript in 2023, in the edition called ES2023. Modern browsers have them. If your site must also work in older browsers, you can:
- check that a method exists before you use it,
- add a polyfill: extra code that adds a missing method to old browsers,
- or use the copy-then-sort pattern from the section above.
Stable sorting: ties keep their order
GUARANTEEDTwo items are tied when the comparator returns 0 for them. A stable sort keeps tied items in the order they had before the sort.
In JavaScript, array sorting is always stable. This has been a rule of the language since the 2019 edition, 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(", "));Line 7 sorts a copy by age only. Line 8 prints the names: Noah, Leo, Mia, Ava.
- Noah and Leo both have age 20. Noah came first in the list, so he stays before Leo.
- Mia and Ava both have age 30. Mia came first, so she stays before Ava.
A sort key is the property you sort by, such as age or name. Because sorting is stable, you can sort in two steps: first by the less important key, then by the main key. Ties on the main key keep the order from the first step.
One comparator with a tie-breaker is often easier to read, though: a.age - b.age || a.name.localeCompare(b.name).
At a help desk, people with the same priority are served in the order they arrived. Nobody with the same priority jumps ahead. A stable sort treats tied items the same fair way.
- In real life: People with the same priority are served in the order they arrived
- In JavaScript: Items where the comparator returns 0 keep their order
- In real life: A higher priority can move someone ahead
- In JavaScript: A result that is not 0 can change the order
- In real life: Everyone knows the queue rule ahead of time
- In JavaScript: You can count on how a stable sort treats ties
Where you'll use this
Sorting and copying show up wherever the order on screen is different from the order the data was stored in. For example:
- A shop page keeps the products in the order the server sent them, and shows them by price, lowest first.
- A table lets people click Name, Age, or Joined date to sort, without changing the data that was loaded.
- A shopping cart replaces one item and keeps the old cart, so the user can undo.
- Sortable tables
- Leaderboards
- Search results
- Shopping carts
- Undo and redo
- Reports in groups
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; });}sortRows takes the table rows and a sort key:
- Line 3 sorts by name.
- Line 4 sorts by score, highest score first.
- Line 5 returns
0for any other key. Every pair is then a tie, so the rows keep their order.
Because it uses toSorted, sortRows returns a new array every time. The rows passed in never change. That makes it easy to test, and safe to use when you keep old versions for undo.
const rows = [
{ product: "Tea", price: 3 },
{ product: "Bread", price: 2 },
];
const byPrice = rows.toSorted((a, b) => a.price - b.price);
console.log(byPrice.map((row) => row.product).join(", "));
console.log(rows.map((row) => row.product).join(", "));The first line printed is Bread, Tea: the rows in price order, for the screen. The second is Tea, Bread: the rows in the order the server sent them. A sortable table needs exactly this. Make a sorted copy to show, and keep the loaded data as it was.
Sorting compares items many times, so a longer list takes more work. For normal lists on a page, a clear comparator and one sort per click are fine. Don't sort again every time the page redraws if the list and the sort key have not changed.
Common misconceptions
"sort knows that numbers are numbers."
Not by default. With no comparator, sort compares the items as text, so 100 comes before 9.
"A comparator can return true or false."
A comparator must return a number: negative, positive, or zero. true and false become 1 and 0, so there is never a negative answer, and the order can come out wrong.
"sort and reverse give me a new array."
They change the original array and return that same array. Use toSorted or toReversed, or copy the array first.
"Stable means the original array doesn't change."
Stable only means tied items keep their order. sort is stable and changes the original. toSorted is stable and returns a copy.
"undefined is sorted like the word undefined."
sort always puts undefined items after all the other items. They are not turned into the text "undefined". When you print the array with join, they show up as empty spots.
const tasks = ["email", undefined, "call"];
tasks.sort();
console.log(tasks.join(" | "));Line 2 sorts the tasks. The two texts come first, in order: call, then email. The undefined task goes last. Line 3 joins the items with " | " between them, and join shows undefined as nothing. So the output is call | email | .
If you want a missing value to show a word, such as "none", change it to that word before you join or show the list.
Practice: sorting safely
5 EXERCISESRun the code and check what it prints. Then replace toSorted with sort. Does the printed line change? Does scores change?
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 leaves scores as it was. The comparator sorts by number value, so the code prints 1, 9, 10, 100.
Sort the full names by last name. When two last names are the same, sort those two by first name. Use one comparator with a tie-breaker.
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 when it is not 0. When two last names are the same, that comparison gives 0, so || moves on and compares the first names.
Other code still needs this list in its first order. Use a method that returns a sorted copy, so the first line printed 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 line printed shows that original still has its first order. You can show the sorted copy somewhere else.
Replace sugar with honey. The original cart must not change.
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 index 1 in the new array only. The original cart still has sugar.
Before you run it, write down exactly what the default sort will print.
console.log([10, 9, 1].sort().join(", "));console.log([10, 9, 1].sort().join(", "));As text, 1 comes first. 10 also starts with 1 but is longer, so it comes next. 9 comes last. So the code prints 1, 10, 9.
Quiz: check your understanding
7 QUESTIONSRead each explanation, even when you got the answer right. Sorting mistakes often look correct on short lists.
Question 1 of 7What does sorting these numbers with no comparator 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 from smallest to largest?
Choose an answer to see the explanation.
Question 3 of 7Is the array returned by sort the same array?
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 copy with one item replaced?
Choose an answer to see the explanation.
Question 5 of 7What do these two lines 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 promise?
Choose an answer to see the explanation.
Question 7 of 7What does this print?
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 items as text. To sort numbers, give it a comparator. - A comparator returns a negative number to put
afirst, a positive number to putbfirst, and0for a tie. sort,reverse,splice, andarray[index] = valuechange the original array.toSorted,toReversed,toSpliced, andwithreturn a changed copy and leave the original alone.- JavaScript sorting is stable: tied items keep the order they had before.
Remember the one-liner.
Sort with a clear rule, and sort a copy when other code shares the array.
Up next: Iterating arrays & array-likes.