[undefined, null, NaN].sort(); или как не надо сортировать массивы на JavaScript

Как не надо сортировать массивы на JavaScript

Сортировка массивов — это одна из вещей, о которых ты не слишком задумываешься, пока она не перестает работать. Недавно я работал с массивом объектов на JavaScript, который отказывался верно сортироваться и ломал этим абсолютно весь интерфейс. Выяснение что же с ним было не так заняло у меня невероятно много времени, так что я решил поделиться тем, что я в итоге обнаружил.

Специализация «Frontend-разработчик»
Идет набор в группу 5900₽ в месяц

Стандартная сортировка

В JavaScript для массивов есть метод sort, и если вы его используете, то скорее всего получите то, что вы ожидали. Например:

const stringArray = ['cat', 'dog', 'ant', 'butterfly'];
stringArray.sort();
// => [ 'ant', 'butterfly', 'cat', 'dog' ]

Этот метод работает весьма неплохо даже если вы сортируете массивы, в которых могут быть undefined элементы. На сайте MDN утверждается, что “все undefined элементы сортируются в конец массива”.

const stringArrayWithUndefined = [
  'cat',
  undefined,
  'dog',
  undefined,
  'ant',
  'butterfly',
  'zebra'
];
stringArrayWithUndefined.sort();
// => [ 'ant', 'butterfly', 'cat', 'dog', 'zebra', undefined, undefined ]

Подводные камни

Первая проблема с сортировкой может возникнуть, если вы столкнетесь с массивом, содержащим  null.

const stringArrayWithUndefinedAndNull = [
  'cat',
  undefined,
  'dog',
  undefined,
  'ant',
  null,
  'butterfly',
  'zebra'
];
stringArrayWithUndefinedAndNull.sort();
// => [ 'ant', 'butterfly', 'cat', 'dog', null, 'zebra', undefined, undefined ]

Сортировка отправит null в строку «null», которая появится где-то в середине алфавитного порядка. 

Кроме null стоит помнить и о числах. По умолчанию алгоритм сортировки преобразует все элементы массива в строки и затем сравнивает их посимвольно согласно значениям элементов в UTF-16. Это отлично работает в массивах строк, которые мы уже рассмотрели, но алгоритм ломается на числах.

const numberArray = [5, 3, 7, 1];
numberArray.sort();
// => [ 1, 3, 5, 7 ]

const biggerNumberArray = [5, 3, 10, 7, 1];
biggerNumberArray.sort();
// => [ 1, 10, 3, 5, 7 ]

В примере выше сортировка ставит десять перед тройкой, потому что при сравнении посимвольно строка “10” должна стоять перед строкой “3”.

Мы можем исправить это, если предоставим JavaScript функцию сравнения для выполнения сортировки. Функция должна получать два элемента из массива и возвращать числовое значение. Будет ли это значение больше, меньше или равно нулю определит как элементы сортируются относительно друг друга. Если возвращаемое значение меньше нуля, то первый элемент сортируется на место перед вторым, если значение выше нуля, то второй элемент занимает место перед первым. Если возвращаемое значение равно 0, то элементы остаются в том же порядке по отношению друг к другу.

Специализация «Backend-разработчик»
Идет набор в группу 7 400₽ в месяц

Функция сравнения, которая отсортирует числа в порядке возрастания, выглядит достаточно просто:

const compareNumbers = (a, b) => a - b;

Вычитание первого элемента из второго удовлетворяет нужным нам свойствам. Использование этой функции сравнения с нашим массивом biggerNumberArray из примера выше приводит к корректной сортировке чисел.

biggerNumberArray.sort(compareNumbers);
// => [ 1, 3, 5, 7, 10 ]

Это решение все еще работает, если в массиве есть undefined элементы, они игнорируются сортировкой и отправляются в конец.

const numberArrayWithUndefined = [5, undefined, 3, 10, 7, 1];
numberArrayWithUndefined.sort(compareNumbers);
// => [ 1, 3, 5, 7, 10, undefined ]

Но null снова подкидывает проблем.

const numberArrayWithUndefinedAndNull = [5, undefined, 3, null, 10, 7, 1];
numberArrayWithUndefinedAndNull.sort(compareNumbers);
// => [ null, 1, 3, 5, 7, 10, undefined ]

Это происходит из-за того, что преобразование null в число возвращает 0.

Number(null);
// => 0

Вы можете попытаться учесть это в своей функции compareNumbers или просто радоваться тому, что это эта ошибка появляется каждый раз и поэтому не станет неприятной неожиданностью.

Неожиданные сюрпризы

Самая большая проблема, с которой я и столкнулся недавно, возникает когда undefined элемент подкрадывается с другой стороны. Как мы уже видели, если массив содержит undefined, то этот элемент игнорируется и помещается в конец. Но если вы сортируете объекты, ключи которых могут быть undefined, то автоматическая сортировка не срабатывает и полученные результаты могут вас удивить.

Например, если у вас есть массив объектов, где некоторые из них имеют значения, а некоторые нет, и вы пытаетесь отсортировать этот массив по значениям, то результат будет совсем не тот, который вы ожидаете.

const objectArray = [
  { value: 1 },
  { value: 10 },
  {},
  { value: 5 },
  { value: 7 },
  { value: 3 }
];
const compareObjects = (a, b) => a.value - b.value;
objectArray.sort(compareObjects);
// => [ { value: 1 },
//      { value: 10 },
//      {},
//      { value: 3 },
//      { value: 5 },
//      { value: 7 } ]

Вычитание числа из undefined или вычитание undefined из числа в обоих случаях возвращают NaN. Так как NaN не лежит на школе чисел, которая нужна sort от функции сравнения, результат получается несколько странный. В данном случае проблемный элемент остался в массиве там, где он был изначально, а остальные элементы оказались отсортированными.

Существует несколько способов как побороть эту проблему, но самое важное — это знать, что такое может случиться. В моем случае, когда я столкнулся с таким, я отфильтровал объекты без значений, так как они не несли на тот момент никакой важности.

objectArray
  .filter(obj => typeof obj.value !== 'undefined')
  .sort(compareObjects);
// => [ { value: 1 },
//      { value: 3 },
//      { value: 5 },
//      { value: 7 },
//      { value: 10 } ]

Остерегайтесь сортировки

Вывод из всей этой истории: функция сортировки не так проста, как кажется на первый взгляд. Она отлично работает со строками, а вот с числами уже придется немного повозиться. И раз уж так сложилось, что null и undefined являются примитивами, вам нужно помнить о них и внимательно следить за их преобразованиями в процессе сортировки, чтобы получить результат, который вы хотите.

Оригинал: [undefined, null, NaN].sort();

Перевод: Ухарова Елена

Поделиться:
Опубликовано в рубрике Веб-разработка, Переводные материалыTagged ,

SkillFactory.Рассылка