
Наверняка в своей карьере кодера/инженера-программиста/разработчика/профессионального клавишника вы сталкивались с ситуацией, когда вам приходилось сортировать какие-то данные или элементы пользовательского интерфейса. В большинстве случаев это довольно просто, но иногда это может быть настоящей болью. В этой статье я попытаюсь объяснить, как работает сортировка в мире TypeScript и JavaScript, покажу несколько примеров из реальной жизни и кратко расскажу о нескольких популярных библиотеках, которые могут значительно упростить сортировку.
Базовая сортировка
Давайте начнем с самого простого примера и отсортируем массив строк:
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort();
// -> ['Bravo', 'Lima', 'Tango', 'Zulu']
Это самый простой способ отсортировать массив строк по алфавиту в порядке возрастания. Что, если вместо этого мы захотим отсортировать его от Я до А? Нам нужно передать функцию сравнения:
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort((a, b) => {
if (b > a) return 1;
if (b < a) return -1;
return 0;
});
// -> ['Zulu', 'Tango', 'Lima', 'Bravo']
Сначала это может показаться запутанным (было такое), но поверьте мне, на самом деле это имеет большой смысл, когда вы это понимаете.
Как работает функция сравнения
Функция сравнения принимает два аргумента — текущий элемент и следующий элемент. Он должен возвращать число, которое будет определять, как элементы должны быть отсортированы. Общее правило таково:
- если вы вернете число больше 0 — элемент
bперейдет в начало (получит меньший порядковый номер, чем элементa) - если вы вернете число меньше 0 — элемент
aперейдет в начало (получит меньший порядковый номер, чемb) - если вы вернете 0 — элементы останутся на прежней позиции
С помощью простой сортировки чисел мы можем пойти еще дальше и упростить метод сравнения:
// Ascending order const arr = [1, -6, 8, 20, 3, 3]; arr.sort((a, b) => a - b); // -> [-6, 1, 3, 3, 8, 20]// Descending order const arr = [1, -6, 8, 20, 3, 3]; arr.sort((a, b) => b - a); // -> [20, 8, 3, 3, 1, -6]
Разберем сравнение первых двух элементов массива:
- в первом примере мы делаем
1 - (-6), что оценивается как7— это означает, что элементbдолжен быть перед элементомa(-6должен быть перед1) - во втором примере мы делаем
-6 - 1, что оценивается как-7— это означает, что элементaдолжен быть перед элементомb(1должен быть перед-6)
Затем метод sort продолжает его для других чисел. Как именно? Это зависит от браузера. Как оказалось, разные браузеры используют для этого разные алгоритмы. API и вывод одинаковы, но внутри они сортируют вещи немного по-другому. Если вам нужны доказательства, попробуйте приведенный ниже код в консолях Firefox и Chrome и посмотрите, что возвращается — это не одно и то же.
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort((a, b) => {
console.log(`Comparing ${a} to ${b}`);
if (b > a) return 1;
if (b < a) return -1;
return 0;
});
Если вы хотите копнуть еще глубже — ознакомьтесь с этим отличным объяснением различных алгоритмов сортировки.
ПОДСКАЗКА
Стоит иметь в виду: sort — это изменяемый метод, что означает, что он изменяет исходный массив. Если вы хотите создать новый массив, вы можете распространить его, а затем отсортировать следующим образом:
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
[...word].sort()
Сортировка методом localeCompare
В предыдущих примерах мы просто сравнивали строки и возвращали правильное значение для сортировки строк в массиве. Но есть еще один отличный способ сделать это, к тому же он дает нам дополнительные сверхспособности! Я говорю о методе localeCompare. Что это, спросите вы? Как и в предыдущих примерах, функция вернет число в результате сравнения строк, но это позволит нам принять во внимание языковой стандарт. Это особенно полезно для языков, в которых есть специальные символы, так как они могут занимать другое положение в алфавите. Давайте посмотрим на несколько примеров, чтобы все это имело больше смысла.
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort((a,b) => a.localeCompare(b, 'en'));
// -> ['Bravo', 'Lima', 'Tango', 'Zulu']
Как я упоминал ранее, localeCompare возвращает числовое значение, поэтому, если a стоит перед b в алфавите, это дает отрицательное значение. Если b стоит перед a – это дает положительное значение. Если они одинаковые – получается 0. И мы уже знаем, что это означает для метода sort из раздела How the compare function works.
Но где сверхспособности, спросите вы? Давайте взглянем.
Регион
Если вы имеете дело с несколькими языками в своем приложении/веб-сайте, важно передать языковой стандарт текущего языка при использовании сортировки, так как некоторые символы имеют другое положение в алфавите.
'ö'.localeCompare('z', 'de'); // returns a negative value: in German, ö sorts before z'ö'.localeCompare('z', 'sv'); // returns a positive value: in Swedish, ö sorts after z
Опции
localeCompare имеет еще и третий аргумент — параметры. Их немного, но я покажу вам два, на мой взгляд, наиболее необходимых в повседневной жизни. Если вы хотите узнать больше обо всех них, я настоятельно рекомендую MDN и Технологии в сети.
Корпус
Вы можете передать свойство caseFirst в опции. Если вы установите его как upper, сначала будут сортироваться слова в верхнем регистре (когда они начинаются с одной и той же буквы). Если вы передадите lower — он сначала отсортирует строчные.
const words = ['Tango', 'tango', 'Bravo', 'lima', 'Lima'];words.sort((a,b) => a.localeCompare(b, 'en', {caseFirst: 'upper'})); // -> [ 'Bravo', 'Lima', 'lima', 'Tango', 'tango' ]words.sort((a,b) => a.localeCompare(b, 'en', {caseFirst: 'lower'})); // -> [ 'Bravo', 'lima', 'Lima', 'tango', 'Tango' ]
Числовые значения
Допустим, мы хотим отсортировать числовые строки — если мы не передадим свойство numeric: true, они будут рассматриваться как строки и отсортированы аналогично им — "101" будет стоять перед "2", потому что 1 меньше 2. Посмотрите примеры ниже, чтобы лучше понять понимание.
const words = ['4','1001', '30', '200'];words.sort((a,b) => a.localeCompare(b, 'en')); // -> [ '1001', '200', '30', '4' ]words.sort((a,b) => a.localeCompare(b, 'en', {numeric: true})); // -> [ '4', '30', '200', '1001' ]
Примеры из жизни
Мы рассмотрели теорию (которая, я надеюсь, поможет вам лучше понять, как работает сортировка), теперь давайте сосредоточимся на примерах из реальной жизни. Я хочу показать вам несколько, которые будут использовать только собственный метод sort, а также несколько, которые будут использовать внешние библиотеки, такие как [lodash](<https://lodash.com/>). Пойдем!
Сортировка массива объектов
Допустим, у нас есть массив объектов (каждый объект будет представлять одну собаку), и мы хотим отсортировать эти объекты в алфавитном порядке по свойству breed. Давайте посмотрим, как мы можем это сделать.
// Define the interface for our objects interface Dog { breed: string; name: string; }const dogs: Dog[] = [ { breed: 'German Shepard', name: 'Rex' }, { breed: 'Spaniel', name: 'Marley' }, { breed: 'Bulldog', name: 'Thanos' } ];// Sort based on `breed` property. // We don't have to explicitly type `a` and `b` arguments - Typescipt will infer them from the `dogs` array dogs.sort((a,b) => { if (b.breed < a.breed) return 1; if (b.breed > a.breed) return -1; return 0; });// -> // [ // {breed: 'Bulldog', name: 'Thanos'}, // {breed: 'German Shepard', name: 'Rex'}, // {breed: 'Spaniel', name: 'Marley'} // ]
Как видите, это очень похоже на обычную сортировку строк, с той лишь разницей, что мы сравниваем определенные свойства объектов.
ПОДСКАЗКА
Все зависит от ваших предпочтений (и, возможно, стандартов вашей команды), но вы можете сделать функцию сортировки еще короче:
dogs.sort((a, b) => (b.breed < a.breed ? 1 : b.breed > a.breed ? -1 : 0));
Сортировка по разным значениям
Итак, мы рассмотрели это, но что, если в нашем списке будет два бульдога? Мы можем отсортировать их сначала по breed, а затем по name.
const dogs = [ { breed: 'German Shepard', name: 'Rex' }, { breed: 'Spaniel', name: 'Marley' }, { breed: 'Bulldog', name: 'Thanos' }, { breed: 'Bulldog', name: 'Atreus' } ];// Fist sort based on `breed` then if they are the same, sort by `name` property dogs.sort((a,b) => { if (b.breed < a.breed) return 1; if (b.breed > a.breed) return -1; else { if (b.name < a.name) return 1; if (b.name > a.name) return -1; return 0 } })// -> // [ // {breed: 'Bulldog', name: 'Atreus'}, // {breed: 'Bulldog', name: 'Thanos'}, // {breed: 'German Shepard', name: 'Rex'}, // {breed: 'Spaniel', name: 'Marley'} // ]
Как видите, здесь мы просто добавляем еще одну «ветвь» — если сравниваемые значения совпадают, мы добавляем еще один путь else, который обрабатывает сравнение другого свойства объекта — в данном случае name.
ПОДСКАЗКА
Иногда ваши функции сортировки могут быть довольно сложными, поэтому имеет смысл выделить их в собственные методы и передать их в sort.
// Define the interface for our objects interface Dog { breed: string; name: string; }// It's a good idea to pass the types to arguments, otherwise TS will treat them as type of `any` const sortDogsByBreedAndName = (a: Dog,b: Dog) => { if (b.breed < a.breed) return 1; if (b.breed > a.breed) return -1; else { if (b.name < a.name) return 1; if (b.name > a.name) return -1; return 0 } };dogs.sort(sortDogsByBreedAndName)
Сортировка на основе другого массива
Возвращаясь к нашим любимым собакам, представьте, что мы хотим отсортировать их по breed, а не по алфавиту. Допустим, у нас есть определенный список пород, который необходимо использовать в качестве эталона для логики сортировки. Взгляните ниже, так как это может быть легче понять с помощью примера кода:
// This array shows the order of sorting the dogs - Spaniel should be first // and German Shepard should be last const breedsOrder = ['Spaniel', 'Pug', 'Bulldog', 'Poodle', 'German Shepard']; const dogs = [ { breed: 'German Shepard', name: 'Rex' }, { breed: 'Spaniel', name: 'Marley' }, { breed: 'Bulldog', name: 'Thanos' }, { breed: 'Pug', name: 'Bean' }, { breed: 'Poodle', name: 'Axel' } ];dogs.sort((a,b) => breedsOrder.indexOf(a.breed) - breedsOrder.indexOf(b.breed));// -> // [ // { breed: 'Spaniel', name: 'Marley' }, // { breed: 'Pug', name: 'Bean' }, // { breed: 'Bulldog', name: 'Thanos' }, // { breed: 'Poodle', name: 'Axel' }, // { breed: 'German Shepard', name: 'Rex' } // ]
Что здесь случилось? Метод array.indexOf возвращает число — позицию переданного аргумента в массиве. В этой ситуации, если мы сравним индекс 'German Shepard' (4) и 'Spaniel' (0), мы получим -4, что означает, что 'Spaniel' должен идти первым в нашем методе сортировки.
Внешние библиотеки
Помимо ваших ванильных решений JS/TS, существует множество внешних библиотек, которые предоставляют хороший API, облегчающий сортировку. Ниже я покажу вам примеры из двух из них — [lodash](<https://lodash.com/>) и [fast-sort](<https://github.com/snovakovic/fast-sort>).
лодаш
Lodash — очень популярная служебная библиотека JavaScript. Он имеет множество различных методов, которые очень помогают вам в повседневных задачах разработчика. Он также позволяет использовать несколько вспомогательных функций сортировки.
Сортировать по
Этот метод позволяет сортировать массивы объектов. Вывод точно такой же, как и в разделе Sorting based on the different values ранее. В качестве первого аргумента вы передаете массив для сортировки, а второй — массив свойств объектов, по которым мы хотим отсортировать (в приведенном ниже примере — сортировка сначала по породе, а затем по имени, если породы совпадают).
import { sortBy } from 'lodash';const dogs = [ { breed: 'German Shepard', name: 'Rex' }, { breed: 'Spaniel', name: 'Marley' }, { breed: 'Bulldog', name: 'Thanos' }, { breed: 'Bulldog', name: 'Atreus' } ];sortBy(dogs, ['breed', 'name']); // -> // [ // {breed: 'Bulldog', name: 'Atreus'} // {breed: 'Bulldog', name: 'Thanos'}, // {breed: 'German Shepard', name: 'Rex'}, // {breed: 'Spaniel', name: 'Marley'} // ]
Сортировать по
Этот метод почти идентичен sortBy, за исключением того, что он позволяет указать порядок сортировки итераций. Итак, если мы хотим отсортировать по породе в порядке возрастания, а затем по имени в порядке убывания, мы можем сделать что-то вроде этого:
import { orderBy } from 'lodash';const dogs = [ { breed: 'German Shepard', name: 'Rex' }, { breed: 'Spaniel', name: 'Marley' }, { breed: 'Bulldog', name: 'Thanos' }, { breed: 'Bulldog', name: 'Atreus' } ];orderBy(dogs, ['breed', 'name'], ['asc', 'desc']); // -> // [ // {breed: 'Bulldog', name: 'Thanos'} // {breed: 'Bulldog', name: 'Atreus'}, // {breed: 'German Shepard', name: 'Rex'}, // {breed: 'Spaniel', name: 'Marley'} // ]
быстрая сортировка
Это простая в использовании и гибкая библиотека сортировки с поддержкой TypeScript. Когда дело доходит до API, у него немного другой подход, чем у lodash. Давайте посмотрим, как мы можем решить ту же проблему сортировки, что и раньше:
const dogs = [ { breed: 'German Shepard', name: 'Rex' }, { breed: 'Spaniel', name: 'Marley' }, { breed: 'Bulldog', name: 'Thanos' }, { breed: 'Bulldog', name: 'Atreus' } ];// Sort ascending by breed then by name sort(dogs).asc([ dog => dog.breed, dog => dog.name ]); // -> // [ // {breed: 'Bulldog', name: 'Atreus'} // {breed: 'Bulldog', name: 'Thanos'}, // {breed: 'German Shepard', name: 'Rex'}, // {breed: 'Spaniel', name: 'Marley'} // ]// Sort ascending by breed then descending by name sort(dogs).by([ { asc: dog => dog.breed }, { desc: dog => dog.name } ]);// -> // [ // {breed: 'Bulldog', name: 'Thanos'} // {breed: 'Bulldog', name: 'Atreus'}, // {breed: 'German Shepard', name: 'Rex'}, // {breed: 'Spaniel', name: 'Marley'} //
Обе упомянутые библиотеки имеют, конечно, гораздо больше опций и возможностей. Также существует множество других библиотек, решающих те же или похожие проблемы, и это лишь некоторые из них — match-sorter, sort-array или relevancy. Я призываю вас проверить их документацию и поиграть — поверьте мне, иногда они могут спасти вам жизнь.
Резюме
Я надеюсь, что эта статья придаст вам больше уверенности в том, что касается сортировки в Javascript и Typescript. Мы пошли от основ к более сложным примерам и закончили внешними библиотеками, которые могут снять часть работы с наших плеч. Думаю, теперь ты готов… разобраться… (каламбур на уровне шутки папы).