Skip to main content

Методи масивів з мутацією та без мутації в JavaScript

Мутуючі методи масивів змінюють оригінальний масив безпосередньо. Немутуючі повертають нові дані, не чіпаючи оригінал.

Теорія

TL;DR

  • Мутуючі: push(), pop(), splice(), sort(), reverse() - масив змінюється після виклику
  • Немутуючі: map(), filter(), slice(), concat(), find() - оригінал залишається незмінним
  • Аналогія: мутація - це правка документа напряму; немутуючий підхід - спочатку дублікат, потім правка копії
  • React і Redux вимагають немутуючих методів - мутація обходить виявлення змін повністю
  • Швидке рішення: [...arr].sort() дає відсортовану копію без зміни arr

Швидкий приклад

js
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

js
// Неправильно - мутація, React пропускає ре-рендер const handleAdd = () => { state.items.push(newItem); setState(state); // те саме посилання - React не бачить змін }; // Правильно - новий масив, React виявляє зміни const handleAdd = () => { setState([...state.items, newItem]); };

React порівнює prevState === newState. Мутація зберігає те саме посилання, тому перевірка проходить і компонент залишається застарілим. В оновленнях стану завжди створюй новий масив.

Помилка 2: Очікування, що splice() поверне змінений масив

js
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: Сортування чисел без функції порівняння

js
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: Мутація масиву під час ітерації

js
// Неправильно - пропускає елементи непередбачувано 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 із сортуванням

js
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() - схожі назви, протилежна поведінка

js
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() повертає вибране і не чіпає оригінал.

Підводний камінь сортування чисел

js
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 зберігає оригінал недоторканим.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?