Современные методы массивов JavaScript, заменяющие распространённые шаблоны кода
Узнайте, как более новые методы массивов JavaScript, такие как groupBy, toSorted, with и findLast, заменяют громоздкие альтернативы в виде функций reduce, spread и reverse.
Группировка данных без библиотек: Object.groupBy() и Map.groupBy()
В течение многих лет для разделения массива элементов на категории на основе общего свойства требовалось либо писать собственный аккумулятор функции reduce(), либо использовать lodash.groupBy исключительно для этой задачи.
Теперь спецификация ECMAScript предоставляет встроенные функции группировки через Object.groupBy() и Map.groupBy().
Проблема, которую решает это
Когда вы получаете плоский набор записей из API — транзакции, задачи, данные пользователей и так далее — часто необходимо сначала разделить их на группы перед отображением или обработкой с помощью бизнес-логики.
Раньше: реализация с помощью reduce()
const inventory = [
{ name: 'Apples', type: 'fruit', quantity: 10 },
{ name: 'Bananas', type: 'fruit', quantity: 0 },
{ name: 'Carrots', type: 'vegetable', quantity: 14 },
{ name: 'Broccoli', type: 'vegetable', quantity: 5 }
];
const groupedByReduce = inventory.reduce((acc, item) => {
const key = item.type;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(item);
return acc;
}, {});
Теперь: простой и ясный вызов Object.groupBy
const inventory = [
{ name: 'Apples', type: 'fruit', quantity: 10 },
{ name: 'Bananas', type: 'fruit', quantity: 0 },
{ name: 'Carrots', type: 'vegetable', quantity: 14 },
{ name: 'Broccoli', type: 'vegetable', quantity: 5 }
];
const grouped = Object.groupBy(inventory, item => item.type);
/*
Output:
{
fruit: [
{ name: 'Apples', type: 'fruit', quantity: 10 },
{ name: 'Bananas', type: 'fruit', quantity: 0 }
],
vegetable: [
{ name: 'Carrots', type: 'vegetable', quantity: 14 },
{ name: 'Broccoli', type: 'vegetable', quantity: 5 }
]
}
*/
Лучше использовать Map.groupBy()
Когда ключ группировки — это не простая строка или символ, а более сложный объект или динамическая ссылка, Map.groupBy() является более подходящим решением:
const vipTier = { tier: 'VIP' };
const standardTier = { tier: 'Standard' };
const customers = [
{ name: 'Alice', plan: vipTier },
{ name: 'Bob', plan: standardTier },
{ name: 'Charlie', plan: vipTier }
];
const groupedByPlan = Map.groupBy(customers, user => user.plan);
console.log(groupedByPlan.get(vipTier));
// Returns Alice and Charlie's records directly keyed by reference
Неизменяемые преобразования, которым можно доверять: toSorted(), toReversed(), toSpliced()
Классическая проблема JavaScript заключается в том, что такие методы, как .sort(), .reverse() и .splice(), изменяют массив на месте, вместо того чтобы возвращать новый объект.
В фреймворках, основанных на предсказуемом состоянии — React, Redux, Vue и подобных, прямое изменение массива часто приводит к скрытым ошибкам и к тому, что отрисовка тихо теряет синхронизацию.
Какую проблему это решает
Чтобы избежать этих проблем, раньше разработчики писали защитные копии сначала, например [...array].sort() или array.slice().reverse(). Новая группа методов «изменение массива через копию» полностью устраняет такую избыточную кодовую структуру.
Сравнение двух подходов
// --- The Old Mutating Way ---
const scores = [88, 92, 79, 95];
const sortedMutated = scores.sort((a, b) => a - b);
console.log(scores); // [79, 88, 92, 95] -> Original data was destroyed!
// --- The Modern Non-Mutating Way ---
const originalScores = [88, 92, 79, 95];
const cleanSorted = originalScores.toSorted((a, b) => a - b);
console.log(originalScores); // [88, 92, 79, 95] -> Unchanged
console.log(cleanSorted); // [79, 88, 92, 95] -> Fresh array
Замена splice() на toSpliced()
toSpliced() позволяет удалять, вставлять или заменять элементы в любой позиции, оставляя исходный массив нетронутым:
const tabs = ['Home', 'About', 'Pricing', 'Contact'];
// Remove 'Pricing' (index 2) and insert 'Services' & 'Blog'
const updatedTabs = tabs.toSpliced(2, 1, 'Services', 'Blog');
console.log(tabs); // ['Home', 'About', 'Pricing', 'Contact']
console.log(updatedTabs); // ['Home', 'About', 'Services', 'Blog', 'Contact']
Обновление одного элемента без изменения оригинала: with()
Предположим, вы хотите изменить значение в определенном индексе, не трогая исходный массив. Обычный подход заключается в копировании массива, замене значения по индексу и возвращении результата. with() объединяет всё это в одно простое выражение.
Проблема, которую решает этот метод
При обновлении состояний часто требуется заменить отдельный элемент по его позиции, сохраняя при этом остальную часть массива неизменной.
Сравнение двух подходов
const months = ['Jan', 'Mar', 'Mar', 'Apr'];
// Old way: Spread and mutate copy
const fixedMonthsOld = [...months];
fixedMonthsOld[1] = 'Feb';
// Modern way: array.with(index, value)
const fixedMonthsNew = months.with(1, 'Feb');
console.log(months); // ['Jan', 'Mar', 'Mar', 'Apr']
console.log(fixedMonthsNew); // ['Jan', 'Feb', 'Mar', 'Apr']
with() также поддерживает отрицательные индексы, поэтому замена элемента, считаемого от конца, происходит так же просто:
const tags = ['v1.0.0', 'v1.1.0', 'v1.2.0-beta'];
const stableTags = tags.with(-1, 'v1.2.0');
console.log(stableTags); // ['v1.0.0', 'v1.1.0', 'v1.2.0']
Эффективный поиск сзади: findLast() и findLastIndex()
Использование метода .find() для поиска элемента работает хорошо при сканировании с начала, но он всегда начинается с индекса 0. Когда нужен последний найденный элемент, распространённым решением было сначала инвертировать массив — что тратит время и может привести к изменению данных, с которыми не следовало вмешиваться.
Проблема, которую решает этот метод
Данные вроде записей журнала, истории отката действий, ленты активности и записей транзакций естественным образом сортируются по времени, и часто требуется самая свежая запись, соответствующая определенному условию.
Сравнение двух подходов
const logs = [
{ id: 1, action: 'LOGIN', timestamp: 1000 },
{ id: 2, action: 'UPLOAD', timestamp: 1010 },
{ id: 3, action: 'ERROR', timestamp: 1020 },
{ id: 4, action: 'UPLOAD', timestamp: 1030 },
{ id: 5, action: 'LOGOUT', timestamp: 1040 }
];
// Old way: Reversing creates overhead and array allocation
const lastUploadOld = [...logs].reverse().find(log => log.action === 'UPLOAD');
// Modern way: findLast iterates backwards directly
const lastUpload = logs.findLast(log => log.action === 'UPLOAD');
const lastUploadIdx = logs.findLastIndex(log => log.action === 'UPLOAD');
console.log(lastUpload); // { id: 4, action: 'UPLOAD', timestamp: 1030 }
console.log(lastUploadIdx); // 3
Поскольку этот метод сканирует данные с конца и останавливается при нахождении соответствия, его работа занимает O(k) времени, где k — количество шагов назад до нахождения соответствия; нет необходимости сначала копировать или переворачивать весь массив.
5. Совместное использование Map и Filter: flatMap()
Часто возникает необходимость объединять методы .map() с .filter() или .flat(), когда каждый исходный элемент может привести к нулю, одному или нескольким результатам. Метод flatMap() объединяет эту цепочку операций в один проход.
Что это решает: избегает создания временных промежуточных массивов, когда необходимо изменить структуру вложенных данных или одновременно выполнить фильтрацию и преобразование.
Пример: разворачивание вложенного списка связей
const authors = [
{ name: 'Author A', books: ['Book 1', 'Book 2'] },
{ name: 'Author B', books: ['Book 3'] },
{ name: 'Author C', books: [] }
];
// Map + Flat produces 2 array allocations
const booksNested = authors.map(a => a.books).flat();
// flatMap runs the map and flattens depth-1 in one pass
const allBooks = authors.flatMap(a => a.books);
console.log(allBooks); // ['Book 1', 'Book 2', 'Book 3']
Пример: одновременное преобразование и фильтрация
Элемент можно удалить, вернув пустой массив, а сохранить — вернув массив из одного элемента:
const rawInputs = ['42', 'invalid', '100', 'undefined', '256'];
const validNumbers = rawInputs.flatMap(input => {
const num = parseInt(input, 10);
return Number.isNaN(num) ? [] : [num];
});
console.log(validNumbers); // [42, 100, 256]
6. Более удобный доступ к элементам: at()
Для получения последнего элемента массива традиционно использовалась конструкция array[array.length - 1], что неудобно и подвержено ошибкам.
at() использует конвенцию отрицательных индексов, знакомую по Python, позволяя считать назад от конца как массивов, так и строк.
До и после
const queue = ['Alice', 'Bob', 'Charlie', 'Dana'];
// Old way
const lastPersonOld = queue[queue.length - 1];
const secondLastOld = queue[queue.length - 2];
// Modern way
const lastPerson = queue.at(-1); // 'Dana'
const secondLast = queue.at(-2); // 'Charlie'
Результат получается более кратким, менее подверженным ошибкам типа «один единицей меньше» в вычисляемых выражениях, и хорошо совместим с цепочками вызовов методов, поскольку не требуется сначала сохранять массив во временной переменной.
Когда использовать его
Чек-лист модернизации
- Проверьте способ управления состоянием: замените
.sort()и.reverse()на.toSorted()и.toReversed(), чтобы избежать ошибок бесшумной мутации. - Удалите шаблонные функции-редьюсеры: используйте
Object.groupBy()вместо ручно создаваемых объектов-аккумуляторов. - Ускорьте поиск: замените подходы с сначала обратным порядком, затем поиском на
findLast()для данных в временном порядке.
.at(-1) и .with(index, value) вместо ручных вычислений длины и создания защитных копий массивов.Связанные статьи
- Девять распространённых паттернов, вызывающих ненужные перерисовки в React — объясняют девять типичных паттернов работы с состоянием и эффектами в React, которые тайно расширяют область перерисовки, и как реструктурировать компоненты для локализации обновлений.
- Десять скрытых проблем компонентов React, замедляющих современные приложения — рассматривают десять распространённых ошибок в компонентах React — от пробелов в семантическом HTML до отсутствия мемоизации — и способы их устранения для обеспечения высокой скорости, доступности и отсутствия ошибок в приложениях в 2026 году.