Какие методы мутируют массив, а какие нет?

👨‍💻 Frontend Developer 🟠 Может встретиться 🎚️ Средний
#JavaScript #массивы #База JS

Краткий ответ

Мутирующие методы изменяют исходный массив: push(), pop(), shift(), unshift(), splice(), sort(), reverse(), fill(). Немутирующие методы возвращают новый массив, не изменяя исходный: map(), filter(), slice(), concat(), join(), find(), includes(), indexOf(). Понимание этого различия критически важно для предотвращения неожиданных побочных эффектов в коде.

Ключевые принципы:

  • Мутирующие — изменяют оригинал
  • Немутирующие — создают копию
  • Функциональное программирование предпочитает немутирующие методы
  • Производительность — мутирующие методы быстрее

Что такое мутация массива

Мутация массива — это изменение исходного массива “на месте”. Когда метод мутирует массив, он изменяет содержимое оригинального объекта, а не создает новый.

Почему это важно

// Пример с мутацией
const originalArray = [1, 2, 3];
const reference = originalArray;
 
originalArray.push(4); // Мутирующий метод
console.log(originalArray); // [1, 2, 3, 4]
console.log(reference); // [1, 2, 3, 4] — тоже изменился!
 
// Пример без мутации
const originalArray2 = [1, 2, 3];
const reference2 = originalArray2;
 
const newArray = originalArray2.concat(4); // Немутирующий метод
console.log(originalArray2); // [1, 2, 3] — не изменился
console.log(reference2); // [1, 2, 3] — не изменился
console.log(newArray); // [1, 2, 3, 4] — новый массив

Мутирующие методы

1. push() — добавление в конец

const fruits = ['яблоко', 'банан'];
const length = fruits.push('апельсин');
 
console.log(fruits); // ['яблоко', 'банан', 'апельсин'] — изменился!
console.log(length); // 3 — возвращает новую длину
 
// Добавление нескольких элементов
fruits.push('груша', 'киви');
console.log(fruits); // ['яблоко', 'банан', 'апельсин', 'груша', 'киви']

Особенности:

  • ✅ Изменяет исходный массив
  • ✅ Возвращает новую длину массива
  • ✅ Быстрый способ добавления в конец

2. pop() — удаление с конца

const numbers = [1, 2, 3, 4, 5];
const removed = numbers.pop();
 
console.log(removed); // 5 — удаленный элемент
console.log(numbers); // [1, 2, 3, 4] — изменился!
 
// Удаление из пустого массива
const empty = [];
const result = empty.pop();
console.log(result); // undefined
console.log(empty); // [] — остался пустым

3. shift() — удаление с начала

const colors = ['красный', 'зеленый', 'синий'];
const first = colors.shift();
 
console.log(first); // 'красный'
console.log(colors); // ['зеленый', 'синий'] — изменился!

4. unshift() — добавление в начало

const animals = ['кот', 'собака'];
const length = animals.unshift('птица');
 
console.log(animals); // ['птица', 'кот', 'собака'] — изменился!
console.log(length); // 3
 
// Добавление нескольких элементов
animals.unshift('рыба', 'хомяк');
console.log(animals); // ['рыба', 'хомяк', 'птица', 'кот', 'собака']

5. splice() — универсальное изменение

const letters = ['a', 'b', 'c', 'd', 'e'];
 
// Удаление элементов
const removed = letters.splice(1, 2); // С позиции 1 удалить 2 элемента
console.log(removed); // ['b', 'c'] — удаленные элементы
console.log(letters); // ['a', 'd', 'e'] — изменился!
 
// Добавление элементов
letters.splice(1, 0, 'x', 'y'); // В позицию 1 добавить 'x', 'y'
console.log(letters); // ['a', 'x', 'y', 'd', 'e']
 
// Замена элементов
letters.splice(1, 2, 'z'); // В позиции 1 заменить 2 элемента на 'z'
console.log(letters); // ['a', 'z', 'd', 'e']

6. sort() — сортировка

const numbers = [3, 1, 4, 1, 5, 9];
const sorted = numbers.sort();
 
console.log(numbers); // [1, 1, 3, 4, 5, 9] — изменился!
console.log(sorted); // [1, 1, 3, 4, 5, 9] — та же ссылка
console.log(numbers === sorted); // true
 
// Сортировка чисел по возрастанию
const nums = [10, 5, 40, 25, 1000, 1];
nums.sort((a, b) => a - b);
console.log(nums); // [1, 5, 10, 25, 40, 1000]
 
// Сортировка строк
const words = ['банан', 'яблоко', 'апельсин'];
words.sort();
console.log(words); // ['апельсин', 'банан', 'яблоко']

7. reverse() — переворот

const original = [1, 2, 3, 4, 5];
const reversed = original.reverse();
 
console.log(original); // [5, 4, 3, 2, 1] — изменился!
console.log(reversed); // [5, 4, 3, 2, 1] — та же ссылка
console.log(original === reversed); // true

8. fill() — заполнение

const arr = [1, 2, 3, 4, 5];
 
// Заполнение всего массива
arr.fill(0);
console.log(arr); // [0, 0, 0, 0, 0] — изменился!
 
// Заполнение части массива
const arr2 = [1, 2, 3, 4, 5];
arr2.fill('x', 1, 4); // С позиции 1 до 4 (не включительно)
console.log(arr2); // [1, 'x', 'x', 'x', 5]

Немутирующие методы

1. map() — преобразование

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
 
console.log(numbers); // [1, 2, 3, 4, 5] — не изменился!
console.log(doubled); // [2, 4, 6, 8, 10] — новый массив
 
// Преобразование объектов
const users = [{name: 'Иван', age: 25}, {name: 'Мария', age: 30}];
const names = users.map(user => user.name);
console.log(users); // Исходный массив не изменился
console.log(names); // ['Иван', 'Мария']

2. filter() — фильтрация

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const even = numbers.filter(x => x % 2 === 0);
 
console.log(numbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] — не изменился!
console.log(even); // [2, 4, 6, 8, 10] — новый массив
 
// Фильтрация объектов
const products = [
  {name: 'Хлеб', price: 30},
  {name: 'Молоко', price: 60},
  {name: 'Мясо', price: 500}
];
 
const cheap = products.filter(product => product.price < 100);
console.log(products); // Исходный массив не изменился
console.log(cheap); // [{name: 'Хлеб', price: 30}, {name: 'Молоко', price: 60}]

3. slice() — извлечение части

const fruits = ['яблоко', 'банан', 'апельсин', 'груша', 'киви'];
const part = fruits.slice(1, 4);
 
console.log(fruits); // ['яблоко', 'банан', 'апельсин', 'груша', 'киви'] — не изменился!
console.log(part); // ['банан', 'апельсин', 'груша'] — новый массив
 
// Копирование всего массива
const copy = fruits.slice();
console.log(copy); // ['яблоко', 'банан', 'апельсин', 'груша', 'киви']
console.log(fruits === copy); // false — разные объекты

4. concat() — объединение

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = arr1.concat(arr2);
 
console.log(arr1); // [1, 2, 3] — не изменился!
console.log(arr2); // [4, 5, 6] — не изменился!
console.log(combined); // [1, 2, 3, 4, 5, 6] — новый массив
 
// Объединение с отдельными элементами
const withElements = arr1.concat(7, 8, arr2);
console.log(withElements); // [1, 2, 3, 7, 8, 4, 5, 6]

5. join() — преобразование в строку

const words = ['Привет', 'мир', 'JavaScript'];
const sentence = words.join(' ');
 
console.log(words); // ['Привет', 'мир', 'JavaScript'] — не изменился!
console.log(sentence); // 'Привет мир JavaScript' — строка
console.log(typeof sentence); // 'string'
 
// Разные разделители
const csv = words.join(', ');
console.log(csv); // 'Привет, мир, JavaScript'

6. Методы поиска

const numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
 
// find() — поиск элемента
const found = numbers.find(x => x > 3);
console.log(numbers); // [1, 2, 3, 4, 5, 4, 3, 2, 1] — не изменился!
console.log(found); // 4
 
// indexOf() — поиск индекса
const index = numbers.indexOf(4);
console.log(index); // 3
 
// includes() — проверка наличия
const hasThree = numbers.includes(3);
console.log(hasThree); // true
 
// findIndex() — поиск индекса по условию
const foundIndex = numbers.findIndex(x => x > 4);
console.log(foundIndex); // 4

Сравнительная таблица

МетодМутируетВозвращаетНазначение
push()✅ ДаНовая длинаДобавление в конец
pop()✅ ДаУдаленный элементУдаление с конца
shift()✅ ДаУдаленный элементУдаление с начала
unshift()✅ ДаНовая длинаДобавление в начало
splice()✅ ДаМассив удаленныхУниверсальное изменение
sort()✅ ДаТот же массивСортировка
reverse()✅ ДаТот же массивПереворот
fill()✅ ДаТот же массивЗаполнение
map()❌ НетНовый массивПреобразование
filter()❌ НетНовый массивФильтрация
slice()❌ НетНовый массивИзвлечение части
concat()❌ НетНовый массивОбъединение
join()❌ НетСтрокаПреобразование в строку
find()❌ НетЭлемент/undefinedПоиск элемента
indexOf()❌ НетИндекс/-1Поиск индекса
includes()❌ НетBooleanПроверка наличия

Практические примеры

1. Работа с состоянием в React

// ❌ Неправильно — мутация состояния
function TodoList() {
  const [todos, setTodos] = useState(['Задача 1', 'Задача 2']);
  
  const addTodo = (newTodo) => {
    todos.push(newTodo); // Мутация!
    setTodos(todos); // React не обнаружит изменения
  };
  
  return /* JSX */;
}
 
// ✅ Правильно — создание нового массива
function TodoList() {
  const [todos, setTodos] = useState(['Задача 1', 'Задача 2']);
  
  const addTodo = (newTodo) => {
    setTodos([...todos, newTodo]); // Новый массив
    // или
    // setTodos(todos.concat(newTodo));
  };
  
  const removeTodo = (index) => {
    setTodos(todos.filter((_, i) => i !== index)); // Новый массив
  };
  
  return /* JSX */;
}

2. Функциональное программирование

// ❌ Императивный стиль с мутациями
function processUsers(users) {
  const result = [];
  
  for (let i = 0; i < users.length; i++) {
    if (users[i].active) {
      const user = users[i];
      user.displayName = `${user.firstName} ${user.lastName}`; // Мутация!
      result.push(user);
    }
  }
  
  result.sort((a, b) => a.age - b.age); // Мутация!
  return result;
}
 
// ✅ Функциональный стиль без мутаций
function processUsers(users) {
  return users
    .filter(user => user.active)
    .map(user => ({
      ...user,
      displayName: `${user.firstName} ${user.lastName}`
    }))
    .slice() // Создаем копию перед сортировкой
    .sort((a, b) => a.age - b.age);
}

3. Избежание побочных эффектов

// ❌ Опасно — функция изменяет входные данные
function addItemToCart(cart, item) {
  cart.push(item); // Мутация входного параметра!
  return cart;
}
 
const myCart = ['товар1', 'товар2'];
const updatedCart = addItemToCart(myCart, 'товар3');
console.log(myCart); // ['товар1', 'товар2', 'товар3'] — изменился!
 
// ✅ Безопасно — функция не изменяет входные данные
function addItemToCart(cart, item) {
  return [...cart, item]; // Новый массив
  // или
  // return cart.concat(item);
}
 
const myCart2 = ['товар1', 'товар2'];
const updatedCart2 = addItemToCart(myCart2, 'товар3');
console.log(myCart2); // ['товар1', 'товар2'] — не изменился!
console.log(updatedCart2); // ['товар1', 'товар2', 'товар3']

Производительность

Сравнение производительности

function performanceTest() {
  const size = 100000;
  const testArray = Array.from({length: size}, (_, i) => i);
  
  // Тест 1: Мутирующий push vs немутирующий concat
  console.time('Мутирующий push');
  const arr1 = [];
  for (let i = 0; i < size; i++) {
    arr1.push(i);
  }
  console.timeEnd('Мутирующий push');
  
  console.time('Немутирующий concat');
  let arr2 = [];
  for (let i = 0; i < size; i++) {
    arr2 = arr2.concat(i); // Создает новый массив каждый раз!
  }
  console.timeEnd('Немутирующий concat');
  
  // Тест 2: Мутирующий sort vs немутирующий slice+sort
  const unsorted = [...testArray].reverse();
  
  console.time('Мутирующий sort');
  const sorted1 = [...unsorted];
  sorted1.sort((a, b) => a - b);
  console.timeEnd('Мутирующий sort');
  
  console.time('Немутирующий slice+sort');
  const sorted2 = unsorted.slice().sort((a, b) => a - b);
  console.timeEnd('Немутирующий slice+sort');
}
 
performanceTest();

Результаты (приблизительные)

ОперацияМутирующийНемутирующийРазница
Добавление элементовpush() ~1мсconcat() ~500мс500x медленнее
Сортировкаsort() ~10мсslice()+sort() ~12мс1.2x медленнее
Удаление элементаsplice() ~1мсfilter() ~5мс5x медленнее

Лучшие практики

1. Выбор подходящего метода

// ✅ Используйте мутирующие методы для производительности
function buildLargeArray() {
  const result = [];
  for (let i = 0; i < 1000000; i++) {
    result.push(i); // Быстро
  }
  return result;
}
 
// ✅ Используйте немутирующие методы для безопасности
function processUserData(users) {
  return users
    .filter(user => user.active)
    .map(user => ({...user, processed: true})); // Безопасно
}
 
// ✅ Создавайте копию перед мутацией
function sortSafely(array) {
  return [...array].sort(); // Копия + мутация
  // или
  // return array.slice().sort();
}

2. Работа с объектами в массивах

const users = [
  {id: 1, name: 'Иван', active: true},
  {id: 2, name: 'Мария', active: false},
  {id: 3, name: 'Петр', active: true}
];
 
// ❌ Неправильно — мутация объектов
function activateUser(users, userId) {
  const user = users.find(u => u.id === userId);
  if (user) {
    user.active = true; // Мутация объекта!
  }
  return users;
}
 
// ✅ Правильно — создание новых объектов
function activateUser(users, userId) {
  return users.map(user => 
    user.id === userId 
      ? {...user, active: true} // Новый объект
      : user // Тот же объект
  );
}

3. Цепочки методов

// ✅ Безопасная цепочка немутирующих методов
const result = users
  .filter(user => user.age >= 18)
  .map(user => ({...user, adult: true}))
  .slice(0, 10)
  .sort((a, b) => a.name.localeCompare(b.name));
 
// ❌ Опасная цепочка с мутирующими методами
const result2 = users
  .filter(user => user.age >= 18)
  .sort((a, b) => a.name.localeCompare(b.name)) // Мутирует отфильтрованный массив
  .slice(0, 10);

Современные альтернативы

1. Spread оператор

// Вместо мутирующих методов
const arr = [1, 2, 3];
 
// push() → spread
const withPush = [...arr, 4]; // [1, 2, 3, 4]
 
// unshift() → spread
const withUnshift = [0, ...arr]; // [0, 1, 2, 3]
 
// splice() для добавления → spread
const withInsert = [...arr.slice(0, 1), 'new', ...arr.slice(1)];
// [1, 'new', 2, 3]
 
// concat() → spread
const arr2 = [4, 5, 6];
const combined = [...arr, ...arr2]; // [1, 2, 3, 4, 5, 6]

2. Современные методы

// toSorted() — немутирующая сортировка (ES2023)
const numbers = [3, 1, 4, 1, 5];
const sorted = numbers.toSorted(); // Новый массив
console.log(numbers); // [3, 1, 4, 1, 5] — не изменился
console.log(sorted); // [1, 1, 3, 4, 5]
 
// toReversed() — немутирующий переворот (ES2023)
const reversed = numbers.toReversed(); // Новый массив
console.log(numbers); // [3, 1, 4, 1, 5] — не изменился
console.log(reversed); // [5, 1, 4, 1, 3]
 
// with() — немутирующая замена элемента (ES2023)
const updated = numbers.with(0, 999); // Заменить элемент по индексу 0
console.log(numbers); // [3, 1, 4, 1, 5] — не изменился
console.log(updated); // [999, 1, 4, 1, 5]

3. Библиотеки для иммутабельности

// Immutable.js
import { List } from 'immutable';
 
const list = List([1, 2, 3]);
const newList = list.push(4); // Всегда возвращает новый объект
console.log(list.toArray()); // [1, 2, 3]
console.log(newList.toArray()); // [1, 2, 3, 4]
 
// Immer
import produce from 'immer';
 
const state = {items: [1, 2, 3]};
const newState = produce(state, draft => {
  draft.items.push(4); // Выглядит как мутация, но создает новый объект
});
console.log(state.items); // [1, 2, 3]
console.log(newState.items); // [1, 2, 3, 4]

Задачи для практики

Задача 1

// Что выведет этот код?
const arr1 = [1, 2, 3];
const arr2 = arr1;
const arr3 = arr1.slice();
 
arr1.push(4);
arr2.unshift(0);
arr3.pop();
 
console.log(arr1);
console.log(arr2);
console.log(arr3);
Ответ
console.log(arr1); // [0, 1, 2, 3, 4]
console.log(arr2); // [0, 1, 2, 3, 4] — та же ссылка, что и arr1
console.log(arr3); // [1, 2] — независимая копия

Задача 2

// Создайте функцию, которая удаляет элемент по индексу БЕЗ мутации
function removeAtIndex(array, index) {
  // Ваш код
}
 
console.log(removeAtIndex([1, 2, 3, 4, 5], 2)); // [1, 2, 4, 5]
const original = [1, 2, 3, 4, 5];
const result = removeAtIndex(original, 2);
console.log(original); // [1, 2, 3, 4, 5] — не должен измениться
Ответ
function removeAtIndex(array, index) {
  return array.filter((_, i) => i !== index);
  
  // или
  // return [...array.slice(0, index), ...array.slice(index + 1)];
  
  // или
  // return array.slice(0, index).concat(array.slice(index + 1));
}

Задача 3

// Исправьте функцию, чтобы она не мутировала входной массив
function processNumbers(numbers) {
  numbers.sort((a, b) => a - b);
  numbers.reverse();
  return numbers.slice(0, 3);
}
 
const nums = [5, 2, 8, 1, 9, 3];
const top3 = processNumbers(nums);
console.log(nums); // Должен остаться [5, 2, 8, 1, 9, 3]
console.log(top3); // [9, 8, 5]
Ответ
function processNumbers(numbers) {
  return numbers
    .slice() // Создаем копию
    .sort((a, b) => a - b)
    .reverse()
    .slice(0, 3);
    
  // или более эффективно:
  // return numbers
  //   .slice()
  //   .sort((a, b) => b - a) // Сразу по убыванию
  //   .slice(0, 3);
    
  // или с spread:
  // return [...numbers]
  //   .sort((a, b) => b - a)
  //   .slice(0, 3);
}

Заключение

Понимание различий между мутирующими и немутирующими методами массивов критически важно для написания предсказуемого и безопасного кода. Мутирующие методы быстрее, но могут вызывать побочные эффекты. Немутирующие методы безопаснее, но могут быть медленнее для больших объемов данных.

Основные принципы:

  • Используйте немутирующие методы по умолчанию
  • Применяйте мутирующие методы для оптимизации производительности
  • Всегда создавайте копии перед мутацией чужих данных
  • В React и других фреймворках предпочитайте иммутабельность

Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice(@AleksandrEmolov_EasyAdvice), добавляйте сайт в закладки и совершенствуйтесь каждый день 💪