Упорядочивание коллекции
Сортировка означает расстановку элементов коллекции в определённом порядке.
У некоторых типов есть естественный порядок. Числа упорядочены по значению: 1 идёт перед 2. Строки упорядочены по алфавиту: "four" идёт перед "one". Для всего остального или когда вам нужен другой порядок, вы задаёте собственный порядок: функцию сравнения, которая смотрит на два элемента и говорит, какой из них идёт первым.
У массивов есть один метод сортировки, sort(), и две вещи о нём легко упустить:
sort()меняет массив на месте и возвращает тот же самый массив, поэтому исходный порядок теряется. Чтобы сохранить его, сортируйте копию:[...words].sort().- Без функции сравнения
sort()сравнивает строковую форму значений. Для слов это нормально, но[10, 9, 2]получается как[ 10, 2, 9 ], потому что текст"10"идёт перед"2". Для чисел всегда передавайте функцию сравнения.
Функция сравнения получает два элемента, a и b, и возвращает число: отрицательное, когда первым идёт a, положительное, когда первым идёт b, и 0, когда они равны. a - b делает это для чисел, а a.localeCompare(b) — для строк.
sort() устойчива: элементы, которые функция сравнения считает равными, сохраняют свой исходный порядок.
В более новых версиях Node.js есть также toSorted() и toReversed(), которые возвращают новый массив и оставляют оригинал нетронутым. В примерах ниже массив копируется вручную через [...words], что работает везде.
В примерах ниже используются слова one, two, three, four.
Естественный порядок
Естественный порядок — это порядок, который есть у типа сам по себе: числа по значению, строки буква за буквой.
[...words].sort() даёт новый массив в алфавитном порядке. Для убывающего порядка отдельного метода нет: сначала отсортируйте, затем переверните массив с помощью reverse(). Для чисел функция сравнения (a, b) => b - a делает это за один шаг.
У ваших собственных типов объектов естественного порядка нет. Вы задаёте его, написав функцию сравнения, которая возвращает отрицательное число, ноль или положительное число — то же правило, что и для любой другой функции сравнения. Затем вы можете передать её в sort() или использовать напрямую, чтобы сравнить два значения.
Отсортированная копия
[...words].sort() сортирует копию, поэтому сам words выводится после этого без изменений. Последние две строки показывают, зачем числам нужна функция сравнения: без неё [10, 9, 2] становится [ 10, 2, 9 ]; с (a, b) => a - b получается [ 2, 9, 10 ].
Вывод: программа на TypeScript и захваченный вывод
const words = ['one', 'two', 'three', 'four']
// sort() меняет массив, у которого он вызван, поэтому сортируем его копию
const sorted = [...words].sort()
console.log(sorted)
console.log(words)
// без функции сравнения sort() сравнивает строковую форму каждого значения
const numbers = [10, 9, 2]
console.log([...numbers].sort())
console.log([...numbers].sort((a, b) => a - b))
Сортировка по убыванию
Копия сортируется, затем reverse() переворачивает её, поэтому слова идут от "two" вниз до "four". Числа показывают более короткий путь для значений, которые можно вычитать: (a, b) => b - a сортирует [10, 9, 2] сразу в [ 10, 9, 2 ].
Вывод: программа на TypeScript и захваченный вывод
const words = ['one', 'two', 'three', 'four']
// сортируем копию по возрастанию, затем переворачиваем её
const desc = [...words].sort().reverse()
console.log(desc)
// для чисел одна функция сравнения делает ту же работу за один шаг
const numbers = [10, 9, 2]
console.log([...numbers].sort((a, b) => b - a))
Тип со своим собственным порядком
У Version есть числа major и minor. compareVersion() сравнивает сначала major; только когда этот результат равен 0, решает minor, потому что || берёт первый результат, который не равен 0. Проверка compareVersion(...) > 0 отвечает на вопрос «больше ли первая версия?».
Вывод: программа на TypeScript и захваченный вывод
type Version = {
major: number
minor: number
}
// compareVersion возвращает отрицательное число, ноль или положительное число —
// точно так же, как функции сравнения, которые принимает sort().
function compareVersion(a: Version, b: Version): number {
// || берёт первый результат, который не равен 0: сначала major, потом minor
return a.major - b.major || a.minor - b.minor
}
const v12: Version = { major: 1, minor: 2 }
const v13: Version = { major: 1, minor: 3 }
const v20: Version = { major: 2, minor: 0 }
const v15: Version = { major: 1, minor: 5 }
// Больше ли 1.2, чем 1.3?
console.log(compareVersion(v12, v13) > 0)
// Больше ли 2.0, чем 1.5?
console.log(compareVersion(v20, v15) > 0)
Собственные порядки
Собственный порядок — это любой порядок, который вы описываете функцией сравнения.
sort() принимает эту функцию как свой единственный аргумент. Поскольку sort() устойчива, элементы, которые функция считает равными, остаются в своём исходном порядке — именно это вам и нужно при сортировке слов по длине.
Частое желание — отсортировать по одному свойству каждого элемента. Специального метода для этого нет: напишите функцию сравнения, которая сравнивает это свойство, с - для чисел или localeCompare() для строк.
Для убывающего порядка поменяйте местами a и b в сравнении.
Чтобы отсортировать по одному признаку, а затем по другому, соедините два сравнения с помощью ||. Он берёт первый результат, который не равен 0, поэтому, когда первое сравнение считает элементы равными, спор решает второе.
Сортировка с функцией сравнения
Функция сравнения возвращает a.length - b.length. Отрицательный результат означает, что a короче и идёт первым, поэтому слова в итоге идут от самого короткого к самому длинному: [ 'c', 'bb', 'aaa' ].
Вывод: программа на TypeScript и захваченный вывод
const words = ['aaa', 'bb', 'c']
// функция сравнения возвращает a.length - b.length, поэтому короткие слова идут первыми
const sorted = [...words].sort((a, b) => a.length - b.length)
console.log(sorted)
Сортировка по длине
Слова сравниваются с помощью a.length - b.length. "one" и "two" оба длиной 3 буквы; поскольку sort() устойчива, "one" остаётся перед "two".
Вывод: программа на TypeScript и захваченный вывод
const words = ['one', 'two', 'three', 'four']
// sort() устойчив: слова одинаковой длины сохраняют исходный порядок
const byLength = [...words].sort((a, b) => a.length - b.length)
console.log(byLength)
Сортировка по последней букве, по убыванию
a[a.length - 1] — это последняя буква слова. Буквы b и a сравниваются в обратном порядке, поэтому слово с самой большой последней буквой идёт первым: "four" (r), затем "two" (o), затем два слова, оканчивающиеся на «e», в их исходном порядке.
Вывод: программа на TypeScript и захваченный вывод
const words = ['one', 'two', 'three', 'four']
// сравниваем последние буквы, b перед a — для убывающего порядка
const byLastLetter = [...words].sort((a, b) =>
b[b.length - 1].localeCompare(a[a.length - 1]))
console.log(byLastLetter)
Сортировка по длине, затем по алфавиту
|| сначала сравнивает длины. Когда два слова одинаково длинные, он переходит к localeCompare(), который упорядочивает их по алфавиту. Результат выводится через join(' '), в одну строку: a b c aa bb aaa ccc.
Вывод: программа на TypeScript и захваченный вывод
const words = ['aaa', 'bb', 'c', 'b', 'a', 'aa', 'ccc']
// || берёт первое сравнение, которое не равно 0: сначала длина, потом алфавит
const sorted = [...words].sort((a, b) => a.length - b.length || a.localeCompare(b))
console.log(sorted.join(' '))
Проверка отсортированного порядка
Иногда вы хотите лишь узнать, отсортирован ли массив уже, не сортируя его.
Встроенной проверки нет, но every() пишет её в одну строку: каждый элемент должен идти после предыдущего элемента. Небольшой помощник isSorted() ниже принимает функцию сравнения, поэтому один и тот же помощник может проверять возрастающий порядок, убывающий порядок или порядок по одному свойству. Для убывающего порядка вы передаёте функцию сравнения с поменянными местами a и b.
Проверка возрастания и убывания
isSorted() подтверждает, что 1, 2, 3, 4 возрастают. Тот же помощник с (a, b) => b - a подтверждает, что 4, 3, 2, 1 убывают. Элемент с индексом 0 принимается всегда, потому что перед ним нет элемента.
Вывод: программа на TypeScript и захваченный вывод
// isSorted проверяет каждую соседнюю пару с помощью функции сравнения.
function isSorted<T>(items: T[], compare: (a: T, b: T) => number): boolean {
return items.every((value, i) => i === 0 || compare(items[i - 1], value) <= 0)
}
const ascending = [1, 2, 3, 4]
const descending = [4, 3, 2, 1]
console.log(isSorted(ascending, (a, b) => a - b))
// поменянные местами аргументы проверяют убывающий порядок
console.log(isSorted(descending, (a, b) => b - a))
Проверка порядка по полю
Пользователи — это Alice (24), Bob (31) и Charlie (29). Функция сравнения смотрит только на age. Bob старше, чем Charlie, поэтому массив не отсортирован по возрасту и ответ — false.
Вывод: программа на TypeScript и захваченный вывод
type User = {
name: string
age: number
}
function isSorted<T>(items: T[], compare: (a: T, b: T) => number): boolean {
return items.every((value, i) => i === 0 || compare(items[i - 1], value) <= 0)
}
const users: User[] = [
{ name: 'Alice', age: 24 },
{ name: 'Bob', age: 31 },
{ name: 'Charlie', age: 29 },
]
// упорядочены ли пользователи по возрасту?
console.log(isSorted(users, (a, b) => a.age - b.age))
Обратный порядок
Переворачивание ставит последний элемент первым, а первый элемент — последним.
reverse() переворачивает массив на месте, точно так же, как это делает sort(). Чтобы вместо этого получить перевёрнутую копию, переверните копию: [...words].reverse(). Оригинал сохраняет свой порядок.
Перевёрнутая копия
Копия переворачивается и выводится, затем выводится оригинал, чтобы показать, что он не изменился.
Вывод: программа на TypeScript и захваченный вывод
const words = ['one', 'two', 'three', 'four']
// reverse() тоже работает на месте, поэтому переворачиваем копию
const reversed = [...words].reverse()
console.log(reversed)
console.log(words)
Случайный порядок
Перемешивание ставит элементы в случайном порядке.
Метода перемешивания нет. Обычный способ — небольшой цикл, который идёт по массиву с последней позиции вниз и меняет каждый элемент местами с элементом на более ранней позиции. Цикл меняет тот массив, который ему дали, поэтому перемешивайте копию, когда оригинал вам ещё нужен.
Math.random() давал бы другой порядок при каждом запуске. Поэтому в этом примере создаётся собственный генератор: две строки арифметики, которые превращают seed в поток чисел от 0 до 1. Один и тот же seed всегда даёт один и тот же порядок, что полезно для тестов и для примеров вроде этого.
Перемешанная копия
Генератор с seed 9 перемешивает копию слов и выводит [ 'two', 'four', 'three', 'one' ]. Повторный запуск программы выводит тот же порядок, потому что генератор стартует с того же seed.
Вывод: программа на TypeScript и захваченный вывод
// randomGenerator возвращает функцию, которая выдаёт числа от 0 до 1.
// Один и тот же seed всегда даёт одни и те же числа, поэтому вывод не меняется.
function randomGenerator(seed: number): () => number {
let state = seed
return () => {
state = (state * 48271) % 2147483647
return state / 2147483647
}
}
const words = ['one', 'two', 'three', 'four']
const random = randomGenerator(9)
// Идём по копии с последней позиции вниз и меняем местами с более ранней.
const shuffled = [...words]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1))
const kept = shuffled[i]
shuffled[i] = shuffled[j]
shuffled[j] = kept
}
console.log(shuffled)