← Назад к вопросам
Как достигается иммутабельность в JavaScript?
2.2 Middle🔥 161 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Иммутабельность в JavaScript
Иммутабельность (immutability) — это принцип, когда значение, однажды созданное, не может быть изменено. Вместо изменения существующего значения создаётся новое значение. Это фундаментальный концепт в функциональном программировании и критичен для React.
Почему иммутабельность важна?
1. Предсказуемость
Если значение не меняется, код проще понять:
// Мутабельно (неправильно)
const user = { name: 'Alice', age: 30 };
function increaseAge(u) {
u.age = u.age + 1; // меняем оригинальный объект
return u;
}
const userAfter = increaseAge(user);
console.log(user.age); // 31 — оригинальный объект изменился!
console.log(userAfter.age); // 31
// Это очень запутанно — неясно, что именно меняется
// Иммутабельно (правильно)
function increaseAge(u) {
return { ...u, age: u.age + 1 }; // создаём новый объект
}
const userAfter = increaseAge(user);
console.log(user.age); // 30 — оригинальный не изменился
console.log(userAfter.age); // 31
// Ясно, что создан новый объект
2. Обнаружение изменений в React
// React полагается на иммутабельность для оптимизации
function UserComponent() {
const [user, setUser] = useState({ name: 'Alice' });
const [count, setCount] = useState(0);
// ПЛОХО: мутабельное обновление
const handleBad = () => {
user.name = 'Bob'; // меняем прямо
setUser(user); // React может не заметить изменение!
// React сравнивает ссылки: user === user (true)
// React не знает, что объект изменился
};
// ХОРОШО: иммутабельное обновление
const handleGood = () => {
setUser({ ...user, name: 'Bob' }); // новый объект
// React сравнивает ссылки: newUser !== oldUser (true)
// React видит изменение и перерисовывает
};
return <div>User: {user.name}</div>;
}
Техники достижения иммутабельности
1. Spread operator для объектов
// Создание копии объекта
const original = { name: 'Alice', age: 30, email: 'alice@example.com' };
// Способ 1: Spread operator
const updated = { ...original, age: 31 };
// { name: 'Alice', age: 31, email: 'alice@example.com' }
// Способ 2: Object.assign
const updated2 = Object.assign({}, original, { age: 31 });
// { name: 'Alice', age: 31, email: 'alice@example.com' }
// Способ 3: structuredClone (глубокая копия)
const cloned = structuredClone(original);
2. Spread operator для массивов
const original = [1, 2, 3, 4, 5];
// Добавить элемент
const withNew = [...original, 6];
// [1, 2, 3, 4, 5, 6]
// Обновить элемент по индексу
const updated = original.map((num, i) => (i === 2 ? 99 : num));
// [1, 2, 99, 4, 5]
// Удалить элемент
const removed = original.filter((num, i) => i !== 2);
// [1, 2, 4, 5]
// Заменить диапазон
const replaced = [
...original.slice(0, 1), // [1]
10, 11, 12, // новые элементы
...original.slice(3) // [4, 5]
];
// [1, 10, 11, 12, 4, 5]
3. Методы для иммутабельности
const numbers = [1, 2, 3, 4, 5];
// map, filter, reduce — иммутабельны
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
// push, pop, shift, unshift — мутабельны (ИЗБЕГАЙ!)
// concat, slice — иммутабельны
const added = numbers.concat([6, 7]);
// [1, 2, 3, 4, 5, 6, 7]
const sliced = numbers.slice(1, 4);
// [2, 3, 4]
4. Глубокая копия вложенных объектов
const user = {
name: 'Alice',
address: {
city: 'New York',
zip: '10001'
},
hobbies: ['reading', 'coding']
};
// ПЛОХО: поверхностная копия (shallow copy)
const shallow = { ...user };
shallow.address.city = 'Boston';
console.log(user.address.city); // 'Boston' — оригинальный изменился!
// ХОРОШО: глубокая копия (deep copy)
const deep = {
...user,
address: { ...user.address, city: 'Boston' },
hobbies: [...user.hobbies]
};
console.log(user.address.city); // 'New York' — оригинальный не изменился
// ИЛИ structuredClone для полной копии
const cloned = structuredClone(user);
cloned.address.city = 'Boston';
console.log(user.address.city); // 'New York' — не изменился
5. Обновление вложенных структур
const state = {
user: {
id: 1,
name: 'Alice',
profile: {
bio: 'Developer',
avatar: 'url'
}
},
posts: [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' }
]
};
// Обновить user.profile.bio
const updated1 = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
bio: 'Senior Developer'
}
}
};
// Обновить второй post
const updated2 = {
...state,
posts: state.posts.map((post, i) =>
i === 1 ? { ...post, title: 'Updated Post 2' } : post
)
};
Иммутабельность в React
1. useState с объектами
function UserForm() {
const [user, setUser] = useState({ name: '', email: '' });
// ПЛОХО
const handleBadChange = (field, value) => {
user[field] = value; // мутация!
setUser(user);
};
// ХОРОШО
const handleGoodChange = (field, value) => {
setUser({
...user,
[field]: value
});
};
// ИЛИ лучше — отдельный стейт для каждого поля
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</form>
);
}
2. useReducer с иммутабельностью
const initialState = {
users: [],
loading: false,
error: null
};
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true };
case 'FETCH_SUCCESS':
return {
...state,
users: action.payload,
loading: false,
error: null
};
case 'FETCH_ERROR':
return {
...state,
loading: false,
error: action.payload
};
case 'ADD_USER':
return {
...state,
users: [...state.users, action.payload]
};
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user =>
user.id === action.payload.id
? { ...user, ...action.payload.changes }
: user
)
};
case 'DELETE_USER':
return {
...state,
users: state.users.filter(u => u.id !== action.payload)
};
default:
return state;
}
}
function UserManager() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
{state.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
3. Immer для упрощения иммутабельности
import produce from 'immer';
const state = {
user: { name: 'Alice', age: 30 },
posts: [{ id: 1, title: 'Post 1' }]
};
// С Immer можно писать мутабельный код, он будет иммутабельным
const updated = produce(state, (draft) => {
draft.user.name = 'Bob'; // выглядит как мутация
draft.posts.push({ id: 2, title: 'Post 2' }); // выглядит как мутация
// Но Immer создаст новый объект автоматически
});
console.log(state.user.name); // 'Alice' — оригинал не изменился
console.log(updated.user.name); // 'Bob'
// В React
const [state, setState] = useState(initialState);
const updateName = (newName) => {
setState(
produce((draft) => {
draft.user.name = newName;
})
);
};
Лучшие практики
1. Используй правильные методы
// ✓ Иммутабельные методы массивов
.map(), .filter(), .reduce(), .concat(), .slice()
// ✗ Мутабельные методы (ИЗБЕГАЙ)
.push(), .pop(), .shift(), .unshift(), .splice(), .sort(), .reverse()
2. Структура стейта
// ПЛОХО: глубоко вложенная структура
const state = {
app: {
ui: {
modal: {
isOpen: true,
type: 'edit'
}
}
}
};
// ХОРОШО: плоская структура
const state = {
isModalOpen: true,
modalType: 'edit'
};
// Если нужна вложенность, используй Immer
3. Селекторы для сложного стейта
// С Redux Toolkit или Zustand
const selectUserName = (state) => state.user.name;
const selectUserEmail = (state) => state.user.email;
const selectActivePostIds = (state) =>
state.posts.filter(p => p.active).map(p => p.id);
// Использование
const name = useSelector(selectUserName);
4. Memoization для оптимизации
import { useMemo } from 'react';
function ExpensiveComponent({ items }) {
// Пересчитывается только если items изменился
const filtered = useMemo(
() => items.filter(item => item.active),
[items]
);
return <div>{filtered.length}</div>;
}
Сравнение подходов
const original = {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
};
// Добавить элемент
// 1. Spread + concat
const v1 = {
...original,
items: original.items.concat({ id: 3, name: 'Item 3' })
};
// 2. Spread + spread
const v2 = {
...original,
items: [...original.items, { id: 3, name: 'Item 3' }]
};
// 3. Immer
const v3 = produce(original, draft => {
draft.items.push({ id: 3, name: 'Item 3' });
});
// Все результаты идентичны
Производительность
// Shallow comparison (достаточно для большинства случаев)
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1, a: 1 }; // новый объект, но a не изменился
obj1 === obj2; // false (разные ссылки)
obj1.b === obj2.b; // true (вложенный объект не изменился)
// Это оптимизируется с useMemo и useCallback
const memoized = useMemo(() => ({ a: 1, b: { c: 2 } }), []);
// Объект создаётся один раз
Итог
Иммутабельность в JavaScript достигается:
- Spread operator — {...obj, field: newValue}
- Array методы — map, filter, concat вместо push, pop
- Object.assign — создание копии
- structuredClone — глубокая копия
- Immer — упрощение синтаксиса
Это критично для React, потому что:
- React полагается на сравнение ссылок для обнаружения изменений
- Иммутабельность делает код предсказуемым
- Упрощает отладку и тестирование
- Позволяет эффективно использовать memoization