Современные техники без скрытых болевых точек

За последние пять лет в JavaScript многое изменилось, и скромный массив - прекрасный тому пример. Раньше мы тратили свое время на написание итеративной логики с циклами и флагами, поиск значений или кропотливое копирование элементов из одного массива в другой. Теперь деструктуризация, расширенный синтаксис и аккуратный набор современных методов, использующих обработку на основе функций. При правильном применении эти функции могут помочь нам писать более четкий и лаконичный код - код, который с большей вероятностью переживет будущие циклы разработки без накопления трудно обнаруживаемых ошибок. Применяется плохо, и это просто мешанина из новых модных словечек и старых привычек.

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

1. Клонирование массива

Вы, наверное, уже знаете, что это не работает:

const numbers = [2, 42, 5, 304, 1, 13];
numbersCopy = numbers;

Этот код создает две переменные (numbers и numbersCopy), указывающие на один и тот же объект массива в памяти. Но пока нет необходимости разрывать петли.

Вместо этого используйте оператор распространения (...), чтобы развернуть массив в элементы, а затем вставьте эти элементы в новый массив. Лучше всего то, что достаточно одного утверждения:

const numbers = [2, 42, 5, 304, 1, 13];
const numbersCopy = [...numbers];

Теперь, если у вас есть массив объектов, вы можете захотеть стать более привлекательным. Рассмотрим этот пример:

const objects = [{name:'Sadie', age:12}, {name:'Patrick', age:18}];
const objectsCopy = [...objects];

Здесь копия массива (objectsCopy) содержит те же объекты, что и оригинал (objects). Всего есть два массива и четыре элемента, но всего два объекта. Измените объект в первом массиве, это также повлияет на второй массив.

Этот тип копии называется мелкой копией. В зависимости от ваших потребностей вы можете создать глубокую копию, в которой будут дублироваться объекты. И нет необходимости в зацикливании.

Хитрость заключается в сочетании синтаксиса распространения и метода Array.map(). Оператор распространения расширяет объект до набора свойств, которые затем могут быть объединены в новый объект. Метод map() применяет это преобразование к каждому элементу:

const objects = [{name:'Sadie', age:12}, {name:'Patrick', age:18}];
// Create a deep copy of your array
const objectsCopy = objects.map( element => ({...element}) );

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

2. Удаление дубликатов из массива

В JavaScript есть удобная Set коллекция, в которой не допускается дублирование. Копируя элементы массива в Set и обратно в массив, вы можете легко удалить любые повторяющиеся элементы, не жертвуя встроенными функциями объекта Array. Вот как это происходит:

const numbersWithDuplicates = [2, 42, 5, 42, 304, 1, 13, 2, 13];
// Create a Set with unique values (the duplicates are discarded)
const uniqueNumbersSet = new Set(numbersWithDuplicates);
// Turn the Set back into an array (now with 6 items)
const uniqueNumbersArray = Array.from(uniqueNumbersSet);

С помощью оператора распространения вы можете сжать эту операцию до одного оператора:

const uniqueNumbers = [...new Set(numbersWithDuplicates)];

3. Объединение двух массивов

Да, вы могли бы использовать Array.concat(). Проблема в том, что код не такой интуитивно понятный, как вы ожидаете. Взглянем:

const dates2020 = [new Date(2020,1,10), new Date(2020,5,11)];
const dates2021 = [new Date(2021,1,10), new Date(2021,5,12)];
const dates2022 = [new Date(2022,1,10), new Date(2022,5,10)];
// To combine three arrays, you call concat() twice
const datesCombined = dates2020.concat(dates2021).concat(dates2022);

Более чистый подход - использовать оператор распространения для расширения каждого массива, а затем вставлять эти элементы в новый массив путем присваивания:

const datesCombined = [...dates2020, ...dates2021, ...dates2022];

Теперь нет никаких сомнений в том, какой массив размещается первым, вторым и третьим. Вы даже можете вставить дополнительные буквальные значения между ними, если вам нужно:

const datesCombined =
 [...dates2020, ...dates2021, new Date(2022,1,2), ...dates2022];

4. Сортировка массива по свойству его объектов.

Вы можете написать алгоритм сортировки, который перемещается по массиву и строит новый, но, пожалуйста, немедленно откажитесь от этой идеи. С помощью Array.sort() все, что вам нужно сделать, это указать массиву, как сравнивать два элемента, и он сделает всю тяжелую работу.

Вы устанавливаете правила сортировки, написав функцию сравнения. Ваша функция сравнения получает два элемента (соответствующих двум различным элементам массива), сравнивает их и возвращает число, которое указывает результат:

  • Верните 0, если значения следует считать равными
  • Вернуть любое отрицательное число, если первое значение меньше второго.
  • Вернуть любое положительное число, если первое значение больше второго.

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

const people  = [
 { firstName: 'Joe', lastName: 'Khan', age: 21 },
 { firstName: 'Dorian', lastName: 'Khan', age: 15 },
 { firstName: 'Tammy', lastName: 'Smith', age: 41 },
 { firstName: 'Noor', lastName: 'Biles', age: 33 },
 { firstName: 'Sumatva', lastName: 'Chen', age: 19 },
];

Вот еще один не очень оптимизированный способ написать алгоритм сортировки, который сравнивает их по возрасту:

people.sort( function(a, b) {
  if (a.age < b.age) {
    return -1;
  } else if (a.age > b.age) {
    return 1;
  } else {
    return 0;
  }
});

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

people.sort( (a,b) => a.age - b.age );

Иногда можно использовать существующие методы сравнения, встроенные в примитивы или объекты JavaScript. Например, если вы хотите отсортировать этот пример по фамилии в примере объекта "Люди", не нужно изобретать велосипед. Вместо этого используйте метод String.localeCompare(), например:

people.sort( (a,b) => a.lastName.localeCompare(b.lastName) );

5. Очистка массива

Вы можете подумать, что это то, что вам нужно:

myArray = [];

Но у этого подхода есть пара проблем. Во-первых, это не сработает, если вы объявили myArray с const (обычно это именно то, что вы хотите сделать), потому что это создает совершенно новый объект массива. Во-вторых, если на исходный объект массива ссылается другая переменная, он останется очень живым и доступным.

Вот лучший подход, который удаляет все элементы из массива без замены объекта массива:

myArray.length = 0;

Вопреки тому, что вы могли ожидать (и реальности во многих других языках программирования), length не является свойством только для чтения для массивов JavaScript.

6. Проверка, равен ли один массив другому.

Как узнать, одинаковы ли два массива? Вот наивный подход:

if (myArrayA === myArrayB)

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

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

function areArraysEqual(arrayA, arrayB) {
  if (!Array.isArray(arrayA) || !Array.isArray(arrayB)) {
    // There's a null, undeclared, or non-array objects
    return false;
  }
  else if (arrayA === arrayB) {
    // Shortcut: they're two references pointing to the same array
    return true;
  }
  else if (arrayA.length !== arrayB.length) {
    // They can't match if they have a different item count
    return false;
  }
  else {
    // Time to look closer at each item
    for (let i=0; i<arrayA.length; ++i) {
      if (arrayA[i] !== arrayB[i]) return false;
    }
    return true;
  }
}

Стоит отметить, что для сравнения каждой пары элементов используется стандартный оператор ===. Если ваши массивы содержат объекты, это сравнивает ссылки, а не содержимое. Если вы предпочитаете выполнить глубокое сравнение, есть множество направлений, по которым вы можете пойти. Вы можете написать другой тест, сравнивающий свойства объекта. Вы можете использовать метод isEqual() из такой библиотеки, как Lodash или Underscore.js. Но, пожалуйста, не пытайтесь написать свою собственную процедуру сравнения массивов любых объектов на основе JSON, потому что всегда есть крайние случаи, которые будут преследовать вас в будущем.

7. Удаление элементов неразрушающим способом

Один из старых плохих методов массива JavaScript - splice(), который может вырезать части массива. Проблема в том, что метод splice() изменяет массив на месте. Современная практика предпочитает неразрушающие подходы - функции, которые возвращают новый измененный массив без изменения исходного. Это снижает вероятность побочных эффектов и неожиданных последствий. (Это также упрощает модульное тестирование.)

Вместо использования splice() для удаления элементов из массива вы можете создать новый массив вокруг элементов, которые вам не нужны. Для этого вы используете одноименный метод slice(), который копирует часть массива.

Вот пример, в котором элемент 'walrus' удаляется путем объединения двух частей массива. Исходный массив остается нетронутым:

const animal = ['dog', 'cat', 'seal', 'walrus', 'lion', 'cat'];
// Find where the 'walrus' item is
const walrusPos = animals.indexOf('walrus');
// Join the portion before 'walrus' to the portion after 'walrus'
const animalsSliced =
 [...animals.slice(0, walrusPos), ...animals.slice(walrusPos+1)];
// now animalsSliced has ['dog', 'cat', 'seal', 'lion', 'cat']

Это имеет смысл, если вам нужен целевой способ удаления отдельных элементов. Но если вы хотите работать в обратном порядке и выбирать элементы, которые нужно оставить, жизнь станет проще. Вы можете использовать надежный метод filter() для создания копии массива с элементами, которые выбираются функцией, которую вы пишете. Например, вот функция фильтра, которая получает все объекты людей младше 20 лет и помещает их в новый массив:

const people  = [
 { firstName: 'Joe', lastName: 'Khan', age: 21 },
 { firstName: 'Dorian', lastName: 'Khan', age: 15 },
 { firstName: 'Tammy', lastName: 'Smith', age: 41 },
 { firstName: 'Noor', lastName: 'Biles', age: 33 },
 { firstName: 'Sumatva', lastName: 'Chen', age: 19 },
];
const youngPeople = people.filter( element => element.age < 20 );

Я написал большинство этих примеров для третьего издания Поваренной книги JavaScript. Проверьте это! Чтобы узнать о более современных заменах старых практик JavaScript, прочтите Do This Not That, JavaScript Edition. Чтобы получать рассылку раз в месяц с нашими лучшими техническими историями, подпишитесь на Информационный бюллетень Young Coder.