Episode 1 — Fundamentals / 1.22 — Array Methods

1.22.g — sort()

In one sentence: sort() re-orders the array in place (mutating it), and its default behavior converts elements to strings for lexicographic comparison — which produces surprising results for numbers.

Navigation: ← 1.22.f — slice() and splice() · 1.22.h — flat() and flatMap() →


1. sort() basics

sort() sorts the elements of an array in place and returns the same array (not a copy).

const fruits = ["banana", "apple", "cherry"];
const result = fruits.sort();

console.log(fruits); // ["apple", "banana", "cherry"]  ← mutated!
console.log(result === fruits); // true  ← same reference

2. Default sort is LEXICOGRAPHIC

This is the single most important gotcha with sort:

const nums = [10, 2, 1, 20, 3, 100];
nums.sort();

console.log(nums); // [1, 10, 100, 2, 20, 3]  ← NOT numeric order!

Why? Without a compare function, sort converts every element to a string and compares by Unicode code points:

"1"   < "10"  < "100" < "2"  < "20"  < "3"

The string "10" comes before "2" because "1" (char code 49) < "2" (char code 50).

// More surprising examples
[80, 9, 700, 40, 1, 5, 200].sort();
// [1, 200, 40, 5, 700, 80, 9]

[true, false, true].sort();
// [false, true, true]  — "false" < "true" lexicographically

3. The compare function

To sort correctly, provide a compare function:

arr.sort((a, b) => {
  // return negative → a comes first
  // return 0       → keep original order
  // return positive → b comes first
});
Return valueOrder
< 0a before b
0Keep original relative order
> 0b before a

4. Ascending numbers: (a, b) => a - b

const nums = [10, 2, 1, 20, 3, 100];
nums.sort((a, b) => a - b);

console.log(nums); // [1, 2, 3, 10, 20, 100]  ← correct!

How it works:

  • a - b is negative when a < ba stays first
  • a - b is 0 when a === b → order preserved
  • a - b is positive when a > bb moves first

5. Descending numbers: (a, b) => b - a

const nums = [10, 2, 1, 20, 3, 100];
nums.sort((a, b) => b - a);

console.log(nums); // [100, 20, 10, 3, 2, 1]

6. Sorting strings

Default sort() works for simple ASCII strings:

const names = ["Charlie", "Alice", "Bob"];
names.sort();
console.log(names); // ["Alice", "Bob", "Charlie"]

Case sensitivity problem

const words = ["banana", "Apple", "cherry"];
words.sort();
console.log(words); // ["Apple", "banana", "cherry"]
// Uppercase letters have lower code points than lowercase
// "A" (65) < "b" (98) — so "Apple" comes before "banana"

localeCompare for proper string sorting

const words = ["banana", "Apple", "cherry"];
words.sort((a, b) => a.localeCompare(b));
console.log(words); // ["Apple", "banana", "cherry"]

// Case-insensitive sorting
words.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
console.log(words); // ["Apple", "banana", "cherry"]

Internationalization

const german = ["Zahlen", "Apfel", "Ubung"];
german.sort((a, b) => a.localeCompare(b, "de"));
// Sorted correctly for German locale

7. Sorting objects by property

By number property

const students = [
  { name: "Alice", grade: 85 },
  { name: "Bob", grade: 92 },
  { name: "Carol", grade: 78 },
  { name: "Dave", grade: 96 },
];

// Sort by grade ascending
students.sort((a, b) => a.grade - b.grade);
console.log(students.map(s => `${s.name}: ${s.grade}`));
// ["Carol: 78", "Alice: 85", "Bob: 92", "Dave: 96"]

// Sort by grade descending
students.sort((a, b) => b.grade - a.grade);
console.log(students.map(s => `${s.name}: ${s.grade}`));
// ["Dave: 96", "Bob: 92", "Alice: 85", "Carol: 78"]

By string property

const users = [
  { name: "Charlie", age: 30 },
  { name: "Alice", age: 25 },
  { name: "Bob", age: 35 },
];

users.sort((a, b) => a.name.localeCompare(b.name));
console.log(users.map(u => u.name));
// ["Alice", "Bob", "Charlie"]

Multi-level sort (sort by age, then by name)

const people = [
  { name: "Bob", age: 25 },
  { name: "Alice", age: 25 },
  { name: "Charlie", age: 30 },
  { name: "Dave", age: 25 },
];

people.sort((a, b) => {
  // Primary: sort by age ascending
  if (a.age !== b.age) return a.age - b.age;
  // Secondary: sort by name alphabetically
  return a.name.localeCompare(b.name);
});

console.log(people.map(p => `${p.name} (${p.age})`));
// ["Alice (25)", "Bob (25)", "Dave (25)", "Charlie (30)"]

8. Stable sort (guaranteed since ES2019)

A stable sort preserves the relative order of elements that compare as equal. Since ES2019, Array.prototype.sort is guaranteed to be stable.

const items = [
  { name: "A", priority: 1 },
  { name: "B", priority: 2 },
  { name: "C", priority: 1 },
  { name: "D", priority: 2 },
];

items.sort((a, b) => a.priority - b.priority);
// Stable: A stays before C (both priority 1), B stays before D (both priority 2)
// [A(1), C(1), B(2), D(2)]

Before ES2019, some engines used unstable algorithms (like quicksort). Now you can rely on stability.


9. toSorted() (ES2023) — non-mutating version

toSorted works exactly like sort but returns a new array, leaving the original untouched:

const nums = [3, 1, 4, 1, 5, 9];

const sorted = nums.toSorted((a, b) => a - b);
console.log(sorted); // [1, 1, 3, 4, 5, 9]
console.log(nums);   // [3, 1, 4, 1, 5, 9]  ← unchanged!

This is the preferred approach in modern code, especially in React and functional programming where you should not mutate state.

// Other non-mutating array methods (ES2023):
// toSorted()    — non-mutating sort
// toReversed()  — non-mutating reverse
// toSpliced()   — non-mutating splice
// with(index, value) — non-mutating index assignment

Workaround for older environments

// Before toSorted existed:
const sorted = [...nums].sort((a, b) => a - b);
// or
const sorted2 = nums.slice().sort((a, b) => a - b);

10. sort() mutates — beware!

function getTopStudents(students) {
  // BUG: this mutates the original array!
  return students.sort((a, b) => b.grade - a.grade).slice(0, 3);
}

const allStudents = [
  { name: "Alice", grade: 78 },
  { name: "Bob", grade: 92 },
  { name: "Carol", grade: 85 },
];

const top = getTopStudents(allStudents);
console.log(allStudents);
// Now allStudents is sorted too! Not what the caller expected.

// FIX: copy first
function getTopStudentsSafe(students) {
  return [...students].sort((a, b) => b.grade - a.grade).slice(0, 3);
}

11. Real-world examples

Leaderboard

const players = [
  { name: "Alice", score: 2400 },
  { name: "Bob", score: 3100 },
  { name: "Carol", score: 2800 },
  { name: "Dave", score: 3100 },
];

const leaderboard = [...players]
  .sort((a, b) => {
    if (b.score !== a.score) return b.score - a.score; // desc by score
    return a.name.localeCompare(b.name);               // asc by name (tiebreaker)
  })
  .map((p, i) => `${i + 1}. ${p.name}${p.score}`);

console.log(leaderboard);
// ["1. Bob — 3100", "2. Dave — 3100", "3. Carol — 2800", "4. Alice — 2400"]

Price sorting (e-commerce)

const products = [
  { name: "Laptop", price: 999 },
  { name: "Phone", price: 699 },
  { name: "Tablet", price: 499 },
  { name: "Watch", price: 299 },
];

// Sort by price: low to high
const lowToHigh = [...products].sort((a, b) => a.price - b.price);

// Sort by price: high to low
const highToLow = [...products].sort((a, b) => b.price - a.price);

Sort by date

const events = [
  { name: "Meeting", date: "2024-03-15" },
  { name: "Lunch", date: "2024-03-10" },
  { name: "Workshop", date: "2024-03-20" },
];

events.sort((a, b) => new Date(a.date) - new Date(b.date));
console.log(events.map(e => e.name)); // ["Lunch", "Meeting", "Workshop"]

Sort by boolean (true first)

const tasks = [
  { text: "Task A", urgent: false },
  { text: "Task B", urgent: true },
  { text: "Task C", urgent: false },
  { text: "Task D", urgent: true },
];

tasks.sort((a, b) => Number(b.urgent) - Number(a.urgent));
// Urgent tasks first: [B, D, A, C]

12. Sorting gotchas summary

GotchaExampleFix
Default is lexicographic[10, 2].sort()[10, 2]sort((a, b) => a - b)
Mutates originalarr.sort() changes arr[...arr].sort() or toSorted()
Uppercase before lowercase["b", "A"].sort()["A", "b"]sort((a, b) => a.localeCompare(b))
Returns same arraysorted === original is trueBe aware, not a bug

Key takeaways

  1. sort() sorts in place — it mutates the original array and returns the same reference.
  2. Default sort is lexicographic (string comparison) — always pass a compare function for numbers.
  3. (a, b) => a - b for ascending, (a, b) => b - a for descending.
  4. Use localeCompare for proper string and international sorting.
  5. toSorted() (ES2023) is the non-mutating alternative — prefer it in modern code.
  6. Copy before sorting ([...arr].sort(...)) if you need to preserve the original.

Explain-It Challenge

Explain without notes:

  1. Why does [10, 2, 1].sort() produce [1, 10, 2]?
  2. Write a compare function to sort an array of objects by price in descending order.
  3. How does toSorted differ from sort, and why would you prefer it?

Navigation: ← 1.22.f — slice() and splice() · 1.22.h — flat() and flatMap() →