Back

Немутирующие массивы: написание более безопасного JavaScript-кода

Немутирующие массивы: написание более безопасного JavaScript-кода

Когда вы изменяете массив в JavaScript, вы можете случайно изменить данные, от которых зависят другие части вашего кода. Это создает ошибки, которые трудно отследить. Решение? Используйте немутирующие методы массивов, которые возвращают новые массивы вместо изменения оригинала.

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

Ключевые выводы

  • Немутирующие методы массивов возвращают новые массивы, не изменяя исходные данные
  • Использование неизменяемых операций предотвращает неожиданные побочные эффекты и делает код более предсказуемым
  • Методы как map(), filter() и reduce() являются более безопасными альтернативами мутирующим операциям
  • Оператор spread обеспечивает чистый синтаксис для обычных операций с массивами

Почему неизменяемость важна в JavaScript

Мутация массивов может вызвать неожиданное поведение в ваших приложениях. Когда вы передаете массив в функцию или разделяете его между компонентами, изменения в одном месте влияют на все ссылки на этот массив.

const originalTasks = ['Write code', 'Review PR', 'Deploy'];
const completedTasks = originalTasks;
completedTasks.push('Write tests');

console.log(originalTasks); // ['Write code', 'Review PR', 'Deploy', 'Write tests']
// Исходный массив был изменен!

Это становится особенно проблематичным в React-приложениях, где мутации состояния препятствуют повторному рендерингу компонентов, или в Redux, где состояние должно оставаться неизменяемым.

Мутирующие против немутирующих методов: ключевые различия

Мутирующие методы (избегайте их)

  • push(), pop(), shift(), unshift() - добавляют или удаляют элементы
  • sort() - сортирует массив на месте
  • reverse() - обращает порядок массива
  • splice() - добавляет/удаляет элементы в любой позиции
  • fill() - заполняет массив значением

Немутирующие методы (используйте их)

  • map() - преобразует каждый элемент
  • filter() - сохраняет элементы, соответствующие условию
  • reduce() - объединяет элементы в одно значение
  • slice() - извлекает часть массива
  • concat() - объединяет массивы

Основные немутирующие методы массивов

map(): преобразование без мутации

Вместо использования цикла for, который изменяет массив, map() создает новый массив с преобразованными значениями:

const prices = [10, 20, 30];
const discountedPrices = prices.map(price => price * 0.8);

console.log(prices);          // [10, 20, 30] - не изменен
console.log(discountedPrices); // [8, 16, 24]

filter(): безопасная фильтрация массивов

Удаляйте элементы, не затрагивая исходный массив:

const users = [
  { name: 'Alice', active: true },
  { name: 'Bob', active: false },
  { name: 'Charlie', active: true }
];

const activeUsers = users.filter(user => user.active);
console.log(users.length);       // 3 - исходный не изменен
console.log(activeUsers.length); // 2

reduce(): объединение без побочных эффектов

Вычисляйте значения из массивов без внешних переменных:

const orders = [
  { product: 'Laptop', price: 1200 },
  { product: 'Mouse', price: 25 }
];

const total = orders.reduce((sum, order) => sum + order.price, 0);
// Возвращает 1225, не изменяя orders

slice(): извлечение частей массива

Получайте подмножество массива без использования splice():

const tasks = ['Task 1', 'Task 2', 'Task 3', 'Task 4'];
const firstTwo = tasks.slice(0, 2);
const lastTwo = tasks.slice(-2);

console.log(firstTwo); // ['Task 1', 'Task 2']
console.log(lastTwo);  // ['Task 3', 'Task 4']
console.log(tasks);    // Исходный не изменен

concat(): безопасное объединение массивов

Объединяйте массивы без использования push():

const completed = ['Task 1', 'Task 2'];
const pending = ['Task 3', 'Task 4'];
const allTasks = completed.concat(pending);

// Или используйте оператор spread
const allTasksSpread = [...completed, ...pending];

Лучшие практики немутирующих массивов в JavaScript

1. Замените мутирующие операции

// ❌ Избегайте: мутация с push
const items = [1, 2, 3];
items.push(4);

// ✅ Лучше: создайте новый массив
const newItems = [...items, 4];

2. Объединяйте методы в цепочки для сложных операций

const products = [
  { name: 'Laptop', price: 1200, inStock: true },
  { name: 'Phone', price: 800, inStock: false },
  { name: 'Tablet', price: 600, inStock: true }
];

const affordableInStock = products
  .filter(p => p.inStock)
  .filter(p => p.price < 1000)
  .map(p => p.name);
// Возвращает ['Tablet'], не изменяя products

3. Используйте оператор spread для простых операций

// Удалить элемент по индексу
const removeAt = (arr, index) => [
  ...arr.slice(0, index),
  ...arr.slice(index + 1)
];

// Обновить элемент по индексу
const updateAt = (arr, index, value) => [
  ...arr.slice(0, index),
  value,
  ...arr.slice(index + 1)
];

Более безопасное управление состоянием в React

Немутирующие методы необходимы для обновлений состояния React:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false }
  ]);

  const toggleTodo = (id) => {
    // ✅ Создает новый массив с обновленным объектом
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, done: !todo.done }
        : todo
    ));
  };

  const removeTodo = (id) => {
    // ✅ Фильтрует todo без мутации
    setTodos(todos.filter(todo => todo.id !== id));
  };
}

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

Хотя немутирующие методы создают новые массивы, современные JavaScript-движки хорошо оптимизируют эти операции. Преимущества предсказуемого, безошибочного кода обычно перевешивают незначительные различия в производительности. Для критичного по производительности кода с большими наборами данных рассмотрите использование специализированных библиотек, таких как Immutable.js или Immer.

Заключение

Немутирующие методы массивов делают ваш JavaScript-код более предсказуемым и легким для отладки. Используя map(), filter(), reduce(), slice() и concat() вместо их мутирующих аналогов, вы избегаете побочных эффектов, которые приводят к ошибкам. Этот подход особенно ценен в React-приложениях и при следовании принципам функционального программирования. Начните заменять мутирующие операции в своем коде уже сегодня — ваше будущее “я” скажет вам спасибо.

Часто задаваемые вопросы

Да, но будьте последовательны в каждом контексте. Используйте немутирующие методы для общих данных, управления состоянием и функционального программирования. Мутирующие методы могут быть приемлемы для локальных временных массивов, на которые не будет ссылок в других местах.

Разница в производительности незначительна для большинства приложений. Современные JavaScript-движки эффективно оптимизируют эти операции. Рассматривайте альтернативы только для очень больших наборов данных или критичных по производительности циклов после того, как профилирование подтвердит узкое место.

Используйте оператор spread или slice для создания копии сначала, затем отсортируйте копию. Например, const sorted = [...array].sort() или const sorted = array.slice().sort(). Это сохраняет порядок исходного массива.

Slice является немутирующим и возвращает новый массив, содержащий извлеченные элементы, не изменяя исходный. Splice является мутирующим и напрямую изменяет исходный массив, удаляя или заменяя элементы, и возвращает удаленные элементы.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay