Методи масивів з мутацією та без мутації в JavaScript
Мутуючі методи масивів змінюють оригінальний масив безпосередньо. Немутуючі повертають нові дані, не чіпаючи оригінал.
Теорія
TL;DR
- Мутуючі:
push(),pop(),splice(),sort(),reverse()- масив змінюється після виклику - Немутуючі:
map(),filter(),slice(),concat(),find()- оригінал залишається незмінним - Аналогія: мутація - це правка документа напряму; немутуючий підхід - спочатку дублікат, потім правка копії
- React і Redux вимагають немутуючих методів - мутація обходить виявлення змін повністю
- Швидке рішення:
[...arr].sort()дає відсортовану копію без зміниarr
Швидкий приклад
const original = [3, 1, 2];
// sort() мутує - оригінал змінюється
original.sort();
console.log(original); // [1, 2, 3] - змінився
// spread + sort - оригінал залишається
const nums = [3, 1, 2];
const sorted = [...nums].sort();
console.log(nums); // [3, 1, 2] - не змінився
console.log(sorted); // [1, 2, 3] - новий масивsort() повертає посилання на той самий масив, який він змінив, а не новий. Це несподіванка для тих, хто очікує поведінки як у map().
Ключова різниця
Мутуючі методи змінюють пам'ять масиву напряму. Немутуючі виділяють нову пам'ять, обчислюють значення і повертають результат. Якщо дві змінні вказують на один масив, одна мутація зачіпає обидві. Виявлення змін у React порівнює посилання (arr !== prevArr), тому мутація масиву змушує React думати, що нічого не змінилось, і пропустити повторний рендер.
Коли використовувати
- Мутуючі методи: коли масив належить тільки тобі і жоден інший код не тримає посилання на нього; у щільних циклах, де виділення нових масивів помітно впливає на швидкість
- Немутуючі методи: стандартний вибір в оновленнях стану React, редюсерах Redux і будь-якій функції, яка отримує масив як параметр
- Змішаний підхід: мутуй всередині функції, яка сама створила дані, потім повертай результат як нове значення
Таблиця порівняння
| Метод | Тип | Повертає | Змінює оригінал | Де використовувати |
|---|---|---|---|---|
push() | Мутуючий | Нову довжину | Так | Додавання до власного масиву |
concat() | Немутуючий | Новий масив | Ні | Безпечне об'єднання масивів |
splice() | Мутуючий | Видалені елементи | Так | Видалення/вставка за індексом |
slice() | Немутуючий | Новий масив | Ні | Безпечне отримання частини |
sort() | Мутуючий | Той самий масив | Так | Коли мутація прийнятна |
toSorted() | Немутуючий | Новий масив | Ні | Сортування без побічних ефектів (ES2023) |
reverse() | Мутуючий | Той самий масив | Так | Коли мутація прийнятна |
toReversed() | Немутуючий | Новий масив | Ні | Реверс без побічних ефектів (ES2023) |
map() | Немутуючий | Новий масив | Ні | Трансформація даних (React, Redux) |
filter() | Немутуючий | Новий масив | Ні | Фільтрація даних (React, Redux) |
find() | Немутуючий | Один елемент | Ні | Пошук без змін |
fill() | Мутуючий | Той самий масив | Так | Ініціалізація значень масиву |
Типові помилки
Помилка 1: Мутація стану в React
// Неправильно - мутація, React пропускає ре-рендер
const handleAdd = () => {
state.items.push(newItem);
setState(state); // те саме посилання - React не бачить змін
};
// Правильно - новий масив, React виявляє зміни
const handleAdd = () => {
setState([...state.items, newItem]);
};React порівнює prevState === newState. Мутація зберігає те саме посилання, тому перевірка проходить і компонент залишається застарілим. В оновленнях стану завжди створюй новий масив.
Помилка 2: Очікування, що splice() поверне змінений масив
const arr = [1, 2, 3];
const result = arr.splice(1, 1);
console.log(result); // [2] - ВИДАЛЕНИЙ елемент, не [1, 3]
console.log(arr); // [1, 3] - змінений масив, але не в resultПовернене значення splice() дивує майже всіх, хто стикається з ним вперше. Ти очікуєш змінений масив, а отримуєш видалені елементи. Використовуй сам масив після виклику splice, або переходь на filter() якщо потрібна немутуюча поведінка.
Помилка 3: Сортування чисел без функції порівняння
const numbers = [10, 5, 40, 25];
numbers.sort();
console.log(numbers); // [10, 25, 40, 5] - неправильно, сортування як рядки
numbers.sort((a, b) => a - b);
console.log(numbers); // [5, 10, 25, 40] - правильноБез компаратора sort() перетворює елементи на рядки і порівнює їх коди символів. "40" стоїть перед "5" за алфавітом. Для чисел завжди передавай (a, b) => a - b.
Помилка 4: Мутація масиву під час ітерації
// Неправильно - пропускає елементи непередбачувано
const arr = [1, 2, 3, 4];
arr.forEach(item => {
if (item === 2) arr.splice(arr.indexOf(item), 1);
});
// Правильно - filter створює новий масив
const filtered = [1, 2, 3, 4].filter(item => item !== 2);
console.log(filtered); // [1, 3, 4]Splice під час ітерації зміщує індекси і призводить до пропуску елементів. Використовуй filter() замість нього.
Де зустрічається
- React:
map(),filter(),concat(), spread-оператор для всіх оновлень стану - Redux: редюсери повертають новий стан -
filter(),map(), spread-синтаксис; ніколиpush()абоsplice() - Vue.js: немутуючі методи підтримують реактивну систему у робочому стані
- Node.js/Express: мутуючі методи нормально використовувати всередині обробників маршрутів, які самостійно керують даними
- ES2023:
toSorted(),toReversed(),toSpliced()- вбудовані немутуючі альтернативи, більше не потрібно spread перед сортуванням
Питання на співбесіді
Q: Чому React вимагає немутуючих методів масивів?
A: React порівнює посилання (prevState === newState) для виявлення змін. Мутація масиву зберігає те саме посилання, тому перевірка повертає true і React пропускає ре-рендер. Новий масив завжди має інше посилання.
Q: Яка різниця в продуктивності між мутуючими і немутуючими методами?
A: Немутуючі виділяють нову пам'ять і копіюють дані. Для масивів до 1000 елементів різниця незначна. У щільних циклах з великими масивами мутація може мати значення. Для більшості коду незмінність вартує цього невеликого накладного розходу.
Q: Як зробити sort() немутуючим без ES2023?
A: Спочатку створи копію: [...arr].sort() або arr.slice().sort(). Обидва варіанти дають поверхневу копію, тому сортування копії не змінює оригінал.
Q: (Senior) Чому map() завжди повертає новий масив, а sort() змінює оригінал?
A: sort() переставляє елементи на місці і повторно використовує існуючу пам'ять - це рішення заради продуктивності в оригінальній специфікації. map() виробляє трансформовані значення, і виділення нової пам'яті тут неминуче, бо самі значення змінюються. Різниця пов'язана з алгоритмічною необхідністю не менше, ніж з дизайном.
Приклади
Оновлення стану React із сортуванням
function TodoList({ todos, setTodos }) {
// Неправильно - мутує todos, React не ре-рендериться
const handleSortWrong = () => {
todos.sort((a, b) => a.priority - b.priority);
setTodos(todos); // те саме посилання, без ре-рендеру
};
// Правильно - новий масив, React виявляє зміни
const handleSort = () => {
const sorted = [...todos].sort((a, b) => a.priority - b.priority);
setTodos(sorted);
};
return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}[...todos] створює поверхневу копію масиву. Сортування цієї копії дає нове посилання, яке React бачить як зміну і ре-рендерить компонент.
splice() проти slice() - схожі назви, протилежна поведінка
const arr = [1, 2, 3, 4, 5];
// splice() - мутує, повертає видалені елементи
const removed = arr.splice(1, 2);
console.log(removed); // [2, 3] - що видалили
console.log(arr); // [1, 4, 5] - що залишилось (arr змінився)
// slice() - без мутації, повертає те що вибрали
const arr2 = [1, 2, 3, 4, 5];
const kept = arr2.slice(1, 3);
console.log(kept); // [2, 3] - що вибрали
console.log(arr2); // [1, 2, 3, 4, 5] - не змінивсяsplice() і slice() виглядають схоже, але працюють протилежно. splice() мутує і повертає видалене. slice() повертає вибране і не чіпає оригінал.
Підводний камінь сортування чисел
const prices = [100, 25, 1000, 50];
// Сортування за замовчуванням обробляє числа як рядки
prices.sort();
console.log(prices); // [100, 1000, 25, 50] - алфавітний порядок
// Немутуюче сортування з числовим компаратором
const sortedPrices = [...prices].sort((a, b) => a - b);
console.log(sortedPrices); // [25, 50, 100, 1000] - правильно
console.log(prices); // [100, 1000, 25, 50] - оригінал не змінивсяСортування за замовчуванням порівнює рядки. "1000" стоїть перед "25" за алфавітом. Для числового сортування завжди передавай компаратор. Патерн spread + sort зберігає оригінал недоторканим.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.