$ sudo teach IT
РАЗДЕЛ 7 · УРОК 5

sort() с компаратором: сортируем правильно

sort() без аргументов — это ловушка для новичков. Числа сортируются «как строки» и результат вас удивит. Разберём почему так происходит и как написать правильный компаратор.

⏱ ~20 минут 🎓 Для новичков 📦 JavaScript

🐛 Ловушка: sort() сортирует как строки

Попробуйте угадать, что выведет этот код:

const nums = [10, 2, 1, 20, 3];
nums.sort();
console.log(nums); // Что получим?

Многие скажут: [1, 2, 3, 10, 20]. Но реальный ответ — [1, 10, 2, 20, 3].

Почему?! Потому что sort() по умолчанию преобразует элементы в строки и сортирует их лексикографически (как слова в словаре). В строках «10» идёт раньше «2», потому что символ «1» стоит перед «2» в алфавите.

⚠️ Классическая ловушка: [10, 2, 1].sort() → [1, 10, 2]. Это не баг JavaScript — это документированное поведение. sort() без компаратора сортирует строки. Для чисел всегда нужен компаратор!

Ещё примеры странного поведения без компаратора:

// Все примеры без компаратора
[1, 10, 100, 2, 20, 200].sort()
// → [1, 10, 100, 2, 20, 200]  — "лексикографически" верно, численно нет!

["яблоко", "Банан", "вишня"].sort()
// → ["Банан", "вишня", "яблоко"] — заглавные раньше строчных!

[true, false, true].sort()
// → [false, true, true] — вроде работает, но случайно

🎛️ Компаратор: как объяснить sort порядок

Компаратор — это функция, которую вы передаёте в sort(). Она принимает два элемента a и b и говорит, кто из них должен идти раньше:

Отрицательное число

a идёт ПЕРЕД b (a останется слева)

Ноль

Порядок между a и b не меняется

Положительное число

b идёт ПЕРЕД a (b встанет слева от a)

Для сортировки чисел существует простой трюк — вычесть одно из другого:

const nums = [10, 2, 1, 20, 3];

// По возрастанию: a - b
// Если a < b → отрицательное → a перед b ✓
nums.sort((a, b) => a - b);
console.log(nums); // → [1, 2, 3, 10, 20] — правильно!

// По убыванию: b - a (просто меняем местами)
nums.sort((a, b) => b - a);
console.log(nums); // → [20, 10, 3, 2, 1]

Почему a - b работает? Давайте проверим логику:

  • Если a = 2 и b = 10: 2 - 10 = -8 (отрицательное → 2 перед 10) ✓
  • Если a = 10 и b = 2: 10 - 2 = 8 (положительное → 2 перед 10) ✓
  • Если a = b: 0 (порядок не меняется) ✓

🔤 Сортировка строк правильно

Для строк на русском языке лучше использовать localeCompare — этот метод умеет сравнивать строки с учётом языка и регистра:

const cities = ["Казань", "Москва", "Архангельск", "Владивосток"];

// Без localeCompare — работает, но может удивить с кириллицей
cities.sort((a, b) => a > b ? 1 : a < b ? -1 : 0);

// С localeCompare — правильный способ для русских строк
cities.sort((a, b) => a.localeCompare(b, "ru"));
console.log(cities);
// → ["Архангельск", "Владивосток", "Казань", "Москва"]

// По убыванию
cities.sort((a, b) => b.localeCompare(a, "ru"));
// → ["Москва", "Казань", "Владивосток", "Архангельск"]

👥 Сортировка массива объектов

Самый частый кейс в реальном коде — отсортировать массив объектов по какому-то свойству:

const students = [
  { name: "Карл", grade: 75 },
  { name: "Алиса", grade: 92 },
  { name: "Диана", grade: 88 },
  { name: "Боб", grade: 65 }
];

// Сортировка по оценке по возрастанию
students.sort((a, b) => a.grade - b.grade);
console.log(students.map(s => `${s.name}: ${s.grade}`));
// → ["Боб: 65", "Карл: 75", "Диана: 88", "Алиса: 92"]

// По убыванию (лучшие первые)
students.sort((a, b) => b.grade - a.grade);
// → ["Алиса: 92", "Диана: 88", "Карл: 75", "Боб: 65"]

// По имени (алфавитно)
students.sort((a, b) => a.name.localeCompare(b.name, "ru"));
// → ["Алиса", "Боб", "Диана", "Карл"]

⚠️ ОСТОРОЖНО: sort изменяет исходный массив! В отличие от map, filter и reduce, метод sort мутирует (изменяет) исходный массив прямо на месте. Если вам нужен отсортированный массив, не трогая оригинал — сделайте копию!

📋 Сортировка без изменения исходного массива

Если нужно отсортировать, не трогая оригинал, создайте копию массива перед вызовом sort. Есть несколько способов:

const original = [3, 1, 4, 1, 5, 9, 2, 6];

// Способ 1: spread-оператор (современный, чаще всего используют)
const sorted1 = [...original].sort((a, b) => a - b);

// Способ 2: slice() без аргументов возвращает копию
const sorted2 = original.slice().sort((a, b) => a - b);

// Способ 3: Array.from() — создаёт новый массив
const sorted3 = Array.from(original).sort((a, b) => a - b);

console.log(original); // → [3, 1, 4, 1, 5, 9, 2, 6] — не изменился!
console.log(sorted1);  // → [1, 1, 2, 3, 4, 5, 6, 9] — отсортированная копия

// Начиная с ES2023 есть Array.prototype.toSorted() — возвращает копию
const sorted4 = original.toSorted((a, b) => a - b);

✨ Хорошая практика: в современном коде чаще всего пишут [...arr].sort(comparator). Это явно показывает: «я создаю копию и сортирую её, оригинал не трогаю». Избегайте неожиданных мутаций!

✅ Итоги урока

  • sort() без аргументов сортирует как строки — для чисел даёт неверный результат
  • Компаратор (a, b) => a - b — сортировка чисел по возрастанию
  • Компаратор (a, b) => b - a — сортировка чисел по убыванию
  • Для строк используйте a.localeCompare(b, "ru") чтобы правильно учесть язык
  • Для объектов указывайте в компараторе нужное свойство: a.price - b.price
  • sort изменяет исходный массив! Для сортировки без мутации — [...arr].sort()
  • Логика компаратора: отрицательное → a перед b, ноль → не меняем, положительное → b перед a

В следующем уроке — деструктуризация массивов и объектов: один из самых удобных синтаксисов современного JavaScript. 🎁

Сортировка с компаратором

6 вопросов

Сортировка чисел с компаратором

Дан массив чисел. Напишите функцию sortNumbers, которая возвращает новый массив, отсортированный по возрастанию, не изменяя исходный. Используйте [...arr].sort() с компаратором.

Сортировка студентов по успеваемости

Дан массив студентов: [{ name: 'Иван', grade: 75 }, { name: 'Анна', grade: 92 }, { name: 'Боб', grade: 65 }]. Напишите функцию sortStudents, которая возвращает новый массив студентов, отсортированных по оценке (grade) по убыванию (от лучших к худшим), не изменяя исходный массив.