Sorting Complex Arrays in JavaScript

Sorting arrays and manipulating data in JavaScript is a common, often complex and difficult, part of the work we do as front‑end and full‑stack developers. When working with complex data structures in particular, it requires a deeper understanding of how the sort() method behaves. Sorting arrays with objects, nested properties, or multiple criteria can be tricky, but JavaScript provides powerful tools to handle these scenarios effectively, as long as we understand how to properly use them...
Here, I will explore different techniques for sorting complex arrays in JavaScript, covering common use cases and best practices. By the end, you should have a strong grasp of how to manipulate and organise data efficiently.
Understanding the sort() Method
JavaScript's built‑in Array.prototype.sort() method sorts elements in place and, by default, converts those elements to strings before comparing them. However, this behaviour is not nearly as useful if you are working with numbers or objects.
Basic Sorting with Numbers
Sorting a simple array of numbers might seem straightforward, however, JavaScript's sort() method does not handle numbers as you might expect by default. Let's start with an example:
const numbers = [10, 5, 8, 1, 7];
numbers.sort();
console.log(numbers); // Output: [1, 10, 5, 7, 8] (incorrect sorting)What I'm demonstrating here is the classic issue that sort() presents by default, 10 comes before 5 because sort() converts the elements passed to it to strings before comparing them. In fact, if we added another value like 101 to the array, even that would be placed before 5 when using sort() in it's default configuration.
In order to use sort() to correct sort numbers, we need to use it as a higher‑order function and pass a comparison function into it. Like this:
numbers.sort((a, b) => a - b);
console.log(numbers); // Output: [1, 5, 7, 8, 10] (correct sorting)The comparison function ensures numeric sorting by subtracting b from a. A negative return value means a should come before b, a positive value means b should come before a, and 0 treats the values as equal for this comparison. Since ECMAScript 2019, Array.prototype.sort is specified to be stable, so elements that compare as equal retain their original relative order.
Sorting an Array of Objects
When dealing with an array of objects, it becomes a little more complicated; we must specify which property to sort by in our comparison:
const people = [
{ name: "Maddie", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 20 }
];
people.sort((a, b) => a.age - b.age);
console.log(people);
// Output: [{ name: "Charlie", age: 20 }, { name: "Maddie", age: 25 }, { name: "Bob", age: 30 }]In this way, the array is sorted in ascending order based on the age property.
Sorting Nested Properties
When we're dealing with deeply nested properties, we handle them in much the same way, but just by making sure that reference the correct nested property within the comparison function.
For example, if we have a nested dataset of devices and want to sort them by their price, it would look like this:
const products = [
{ name: "Laptop", details: { price: 1000 } },
{ name: "Phone", details: { price: 700 } },
{ name: "Tablet", details: { price: 500 } }
];
products.sort((a, b) => a.details.price - b.details.price);Really, this is much the same as the example above, we've just moved one level further down the nested data tree.
Sorting by Multiple Criteria
In some cases, sorting by a single property just simply isn't enough. For example, we may need to sort by age, but if two people have the same age, we also want to sort them alphabetically by name. I came across a similar requirement in sorting search results for Virgin Atlantic, where two flights leaving at the same time should also be sorted by the airline name.
Sticking with the people‑and‑age example for consistently, here's how we would handle this multi‑criteria sorting:
people.sort((a, b) => {
if (a.age === b.age) {
return a.name.localeCompare(b.name); // Sort alphabetically
}
return a.age - b.age; // Sort by age first
});This comparator orders records by age first, then uses the name as a secondary key when the ages match. That secondary comparison is distinct from the stable‑sort guarantee.
Case‑Insensitive String Sorting
Sorting by Unicode Value
By default, JavaScript compares strings as sequences of UTF‑16 code units. For the English letters in this example, uppercase letters have lower values than their lowercase counterparts, so this is case‑sensitive ordering.
For example, sorting ["cat", "Cat", "dog", "Dog"] gives ["Cat", "Dog", "cat", "dog"]:
const words = ["cat", "Cat", "dog", "Dog"];
words.sort();
console.log(words); // Output: ["Cat", "Dog", "cat", "dog"]The first letters in this example each occupy one UTF‑16 code unit, with these values:
C
=U+0043D
=U+0044c
=U+0063d
=U+0064
In my experience, this trips up developers all the time, although hopefully it makes sense once you see it in action as above.
Sorting Without Case Sensitivity
For human‑language text, code‑unit order is often not the intended collation. localeCompare() can compare according to a locale, and its sensitivity option can control whether case or accents distinguish values. This example also uses localeCompare() with toLowerCase(), although lowercasing is not a substitute for choosing the required locale and collation options:
const words = ["banana", "Apple", "cherry"];
words.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
console.log(words); // Output: ["Apple", "banana", "cherry"]Lowercasing can be enough for a simple set of English labels, but it does not define collation for every language. With localeCompare(), choose the intended locale and options. For example, a.localeCompare(b, "en", { sensitivity: "accent" }) ignores case whilst still distinguishing accents in English; sensitivity: "base" also ignores accent differences where that locale treats the letters as the same base letter.
To offer an example, in the Swedish alphabet ä comes after z, whereas in English, it could be treated as a variation of a and therefore fall immediately after it in sorting.
So, if we use localeCompare set to sv too, we get this:
const words = ["äpple", "apple", "banana"];
words.sort((a, b) => a.localeCompare(b, "sv"));
console.log(words); // Output: ["apple", "banana", "äpple"]For the Swedish ordering required by this example, using localeCompare() with the sv locale produces a different order from a default code‑unit comparison: ["apple", "banana", "äpple"].
Wrapping Up
The comparison function is where we make the ordering rule explicit: numeric value, a property, or several properties in priority order. Without one, sort() compares string values using UTF‑16 code units.
For names and other human‑language text, decide which locale, case and accent rules the product needs. localeCompare() can express those choices; its default settings are not a promise of case‑insensitive sorting.
Key Takeaways
- Without a comparator,
sort()compares string values by UTF‑16 code units. - Sorting numbers requires explicit subtraction (
a - b) to ensure correct numerical ordering. - Object sorting relies on accessing the correct property and handling nested structures where necessary.
- Multi‑criteria sorting enables prioritisation when sorting by multiple fields.
- Use
localeCompare()with the intended locale and sensitivity options when language, case or accent rules matter. - Understanding default sorting behaviour helps prevent unexpected results and ensures predictable data handling.