Наверняка в своей карьере кодера/инженера-программиста/разработчика/профессионального клавишника вы сталкивались с ситуацией, когда вам приходилось сортировать какие-то данные или элементы пользовательского интерфейса. В большинстве случаев это довольно просто, но иногда это может быть настоящей болью. В этой статье я попытаюсь объяснить, как работает сортировка в мире 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. Мы пошли от основ к более сложным примерам и закончили внешними библиотеками, которые могут снять часть работы с наших плеч. Думаю, теперь ты готов… разобраться… (каламбур на уровне шутки папы).