Работа с массивами в JavaScript теперь редко требует написания цикла for. Язык предоставляет выразительные инструменты, которые превращают вложенные, зашумленные данные в чистые и читаемые преобразования. Это руководство продолжает тему основ, уделяя особое внимание выравниванию (flattening), редукции (reducing), запросам и новым паттернам иммутабельности, которые позволяют оставлять исходные данные нетронутыми.
Выравнивание вложенных структур с помощью flat() и flatMap()
Реальные данные обожают вложенность. Получаете записи из API, парсите CSV или читаете конфигурационный файл — вы часто будете сталкиваться с массивами внутри массивов. Метод flat() предназначен для того, чтобы «схлопнуть» эти слои в единый линейный список.
Вызовите flat() у вложенного массива, и по умолчанию он выровняет его на один уровень вглубь:
const nested = [[1, 2], [3, 4]];
const flat = nested.flat();
// [1, 2, 3, 4]
Исходный массив nested остается точно таким же, каким был. Если вложенность глубже, передайте аргумент глубины, например flat(2) или даже flat(Infinity), если вы не знаете точного количества уровней. Это гораздо лучше, чем писать рекурсивные вспомогательные функции вручную.
Когда нужно не только трансформировать элементы, но и выровнять результат, flatMap() делает и то, и другое за один проход. Он применяет функцию отображения к каждому элементу, а затем выравнивает возвращенные массивы на один уровень. Использование связки .map().flat() тоже работает, но flatMap() пропускает создание промежуточного массива, что экономит память и избавляет от второй итерации. Классический пример использования — разбиение массива предложений на отдельные слова:
const lines = ['hello world', 'foo bar'];
const words = lines.flatMap(line => line.split(' '));
// ['hello', 'world', 'foo', 'bar']
Фильтрация и отображение: в чем разница
filter() создает новый массив, содержащий только те элементы, которые прошли вашу проверку. Callback-функция должна возвращать истинное (truthy) значение, чтобы оставить элемент, или ложное (falsy), чтобы отбросить его. Метод никогда не мутирует исходный массив.
const users = [
{ name: 'Ajay', active: true },
{ name: 'Bina', active: false }
];
const activeUsers = users.filter(u => u.active);
filter() легко перепутать с map(), так как оба метода возвращают новые массивы и принимают callback-функции. Разница заключается в «контракте». map() возвращает новый массив той же длины. Трансформируйте каждый элемент, выполняйте проверку условия, и вы действительно увидите true или false в каждом слоте, потому что map() никогда не отбрасывает элементы — он только изменяет их. filter(), с другой стороны, возвращает потенциально более короткий массив, содержащий только те элементы, которые прошли вашу проверку. Если вы хотите удалить данные, используйте filter(). Если вы хотите трансформировать каждую единицу данных, сохраняя их количество, используйте map().
Свертывание массивов с помощью reduce()
В то время как map() и filter() возвращают массивы, reduce() сводит весь список к одному значению. Он проходит по массиву слева направо, пронося аккумулятор через каждый шаг.
const totals = [10, 20, 30];
const sum = totals.reduce((acc, current) => acc + current, 0);
// 60
Второй аргумент — 0 в примере выше — это начальное значение аккумулятора. Если его пропустить, JavaScript будет использовать первый элемент в качестве начальной точки, начиная итерацию с индекса 1. Это часто сбивает с толку, когда массив пуст, так как вызов reduce() без начального значения на пустом массиве вызывает ошибку TypeError.
reduce() нужен не только для арифметики. Его можно использовать для построения таблиц частотности, слияния объектов, поиска максимальных значений или пропуска данных через цепочку преобразований. Поскольку этот метод очень гибкий, стоит держать callback-функцию компактной и давать переменным понятные имена. Плотный блок reduce() быстро становится труднее для чтения, чем цикл, который он заменил.
Булевы запросы: every() и some()
Иногда вам не нужен новый массив; вам нужен ответ «да» или «нет» относительно коллекции. every() возвращает true только в том случае, если все элементы удовлетворяют вашему условию. Если самый первый элемент не проходит проверку, метод немедленно прекращает работу и возвращает false.
const scores = [85, 92, 78];
const allPassed = scores.every(s => s >= 50);
some() — это зеркальное отражение. Он возвращает true, если хотя бы один элемент проходит тест. Как только совпадение найдено, выполнение прерывается на месте.
const hasPerfectScore = scores.some(s => s === 100);
Эти методы заменяют ручные циклы для проверок валидации. Нужно подтвердить, что все поля формы заполнены? Используйте every(). Нужно узнать, есть ли у какого-либо пользователя права администратора? Используйте some().
Инспекция и обновление без мутации
Современный JavaScript делает большой упор на иммутабельность. Эти методы помогают инспектировать или обновлять массивы, не затрагивая оригинал.
keys() возвращает объект-итератор, содержащий индексы массива. entries() возвращает итератор пар [index, value]. Они особенно хороши в циклах for...of, когда вы хотите использовать чистое деструктурирование:
const fruits = ['apple', 'banana'];
for (const [index, fruit] of fruits.entries()) {
console.log(index, fruit);
}
with() — это новое дополнение к языку, которое заменяет элемент по определенному индексу и возвращает совершенно новый массив. Оно решает ту же задачу, что и присваивание через квадратные скобки — arr[index] = value, — но при этом оставляет исходный массив нетронутым. В таких фреймворках, как React, где прямая мутация нарушает отслеживание изменений, with() является более безопасным путем:
const original = [10, 20, 30];
const updated = original.with(1, 99);
// original: [10, 20, 30]
// updated: [10, 99, 30]
Spread и Rest: два направления ...
Синтаксис spread и rest-параметр используют одни и те же три точки, но перемещают данные в противоположных направлениях.
Используйте spread-оператор, чтобы развернуть массив в отдельные элементы. Это самый чистый способ объединить массивы или клонировать их:
const front = [1, 2];
const back = [3, 4];
const merged = [...front, ...back];
const copy = [...merged];
Вы также можете легко добавить дополнительные элементы: [0, ...front, 99, ...back].
Rest-параметр собирает несколько элементов в один массив. Внутри сигнатуры функции он собирает все оставшиеся аргументы:
function logFirst(first, ...rest) {
console.log('First:', first);
console.log('Rest:', rest);
}
logFirst('a', 'b', 'c');
// First: a
// Rest: ['b', 'c']
Вместе они позволяют справляться с распространенной задачей: принимать вариативные входные данные, обрабатывать их как массив, а затем снова развертывать их в вызовы функций или новые структуры данных.
Главный вывод
Цель этих методов — не впечатлить коллег однострочниками. А в том, чтобы сделать ваши намерения очевидными. Когда другой разработчик видит filter(), он понимает, что вы удаляете элементы. Когда он видит reduce(), он понимает, что вы сводите данные. Когда он видит with(), он знает, что исходный массив где-то остался нетронутым. Освоение этих инструментов означает написание самодокументированного кода, который экономит гораздо больше времени, чем любой «умный» цикл.
