Сучасні методи масивів 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 році.