Sorting arrays in JavaScript has one famous trap, and copying arrays has one famous rule: the copy you get is only as deep as you asked for. This lesson covers the comparator function that fixes the trap, the newer non-mutating sort methods, and which copy technique to reach for when your array contains objects.
Why does sorting numbers give the wrong answer?
Because Array.prototype.sort converts every element to a string and compares those strings. That is exactly right for a list of words and completely wrong for a list of numbers, because '100' comes before '9' as text.
const scores = [10, 9, 100, 1];
console.log([...scores].sort()); // [ 1, 10, 100, 9 ]
console.log([...scores].sort((a, b) => a - b)); // [ 1, 9, 10, 100 ]
The first line is not a bug in the engine, it is the documented default. The fix is to pass a comparator function: a function that receives two elements and returns a negative number when the first should come first, a positive number when it should come second, and zero when the two are equivalent. Subtracting them is the standard shortcut, and it works for numbers, dates and anything else you can subtract.
How do you sort objects by a field?
The same comparator rule applies, except now each side of the comparison is an object and you choose which field to compare. Add a second comparison with || to break ties, which is how you get a stable, predictable order when two records share a score.
const players = [
{ name: 'Asha', score: 42 },
{ name: 'Bala', score: 91 },
{ name: 'Chen', score: 91 },
];
players.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
console.log(players.map((p) => p.name + ':' + p.score).join(', '));
// Bala:91, Chen:91, Asha:42
Read the comparator left to right: sort by score descending first, and when the scores are equal — when the subtraction returns 0, which is falsy — fall through to the name comparison. That tie-break is what keeps the output stable between runs and between engines.
A frequent mistake is writing the comparator as a boolean expression, such as (a, b) => a.score > b.score. A boolean is not a number, so the engine treats every result as zero and the array keeps its original order. The comparator has to return a number, and subtracting the two values is the cheapest way to produce one.
What about sorting text?
For strings, localeCompare is the right tool. The default string comparison uses character codes, which puts every capital letter before every lowercase one — so 'B' sorts before 'a', and any accented or non-Latin text ends up in the wrong place.
console.log(['a', 'B'].sort()); // [ 'B', 'a' ]
console.log(['a', 'B'].sort((x, y) => x.localeCompare(y))); // [ 'a', 'B' ]
console.log(['Zoe', 'ada', 'Bob'].sort((x, y) => x.localeCompare(y)));
// [ 'ada', 'Bob', 'Zoe' ]
The third line is the payoff: with a locale-aware comparison the result reads like an alphabetical list a human would expect, instead of grouping by capitalisation.
What is the difference between sort and toSorted?
sort rewrites the array you call it on. toSorted returns a sorted copy and leaves the original alone, and there is a matching toReversed and toSpliced. These newer methods make “sort without side effects” a one-liner instead of the copy-then-sort dance.
const original = [3, 1, 2];
const ascending = original.toSorted((a, b) => a - b);
const reversed = original.toReversed();
const cut = original.toSpliced(1, 1);
console.log(original); // [ 3, 1, 2 ]
console.log(ascending, reversed, cut); // [ 1, 2, 3 ] [ 2, 1, 3 ] [ 3, 2 ]
The original array is still [3, 1, 2] at the end, which is exactly the guarantee you want when the array came from a function argument or from application state. If you need to support older browsers, [...arr].sort(...) is the equivalent one-liner.
The same split applies to reversing. arr.reverse() rewrites the array in place; arr.toReversed() gives you a reversed copy. One naming habit worth adopting: if the method name starts with to, it returns a new value and leaves the original alone.
Which copy should you use, slice or spread?
For an array of plain values — strings, numbers, booleans — slice() and the spread syntax both give you a new array with the same items, and neither one is connected to the original.
const original = [3, 1, 2];
const copy = original.slice();
copy.push(4);
console.log(original, copy); // [ 3, 1, 2 ] [ 3, 1, 2, 4 ]
const spread = [...original];
console.log(spread === original); // false
slice is the older, method-based spelling; the spread syntax is the newer one and does more than copy, since it can also merge arrays. Pick either for a flat list and keep reading for the moment the items are objects.
What happens when the array holds objects?
Both copy techniques are shallow. The new array is different, but the items inside it are the same objects, so mutating one of those objects changes the original too.
const config = { list: [1, 2] };
const shallow = { ...config };
shallow.list.push(3);
console.log(config.list); // [ 1, 2, 3 ]
const deep = structuredClone(config);
deep.list.push(9);
console.log(config.list, deep.list); // [ 1, 2, 3 ] [ 1, 2, 3, 9 ]
The first three lines show the shallow trap: shallow.list and config.list are the same array, so the push shows up in both. structuredClone, built into the platform, copies the nested structure as well, which is why the final line shows config.list untouched by the change to deep.list. The objects module goes into reference behaviour properly; for now the rule is that a one-level copy is enough for arrays of primitives and not enough for arrays of objects.
Which copy technique should I reach for?
| Technique | Copies one level? | Copies deeply? | Mutates the original? |
|---|---|---|---|
arr.slice() | Yes | No | No |
[...arr] | Yes | No | No |
structuredClone(x) | Yes | Yes | No |
arr.sort() | No | No | Yes |
arr.toSorted() | Yes | No | No |
Try it yourself: write a file called leaderboard.js with the players array above. Sort it by score descending with a name tie-break and print it. Then sort a copy by name ascending with localeCompare and print it, checking that the first array is unchanged afterwards. Finally, build a shallow copy of the players array with the spread syntax, change copy[0].score to zero, print the original array's first score, and notice that it changed too — then repeat it with structuredClone and watch it stay put.