Оператори розповсюдження та залишку в JavaScript: відмінності та приклади
Spread (...) та rest (...) мають однаковий синтаксис, але роблять протилежні речі. Spread розпаковує ітерабельне значення в окремі елементи; rest збирає окремі елементи в один масив.
Теорія
TL;DR
- Spread - це як висипати вміст сумки на стіл: все виходить окремо
- Rest - це як скласти залишки назад у сумку: все збирається разом
- Головна різниця: spread стоїть праворуч (або всередині виклику функції), rest стоїть ліворуч (у параметрах або деструктуризації)
- Копіюєш, об'єднуєш або передаєш дані? Використовуй spread. Захоплюєш зайві аргументи або залишкові пропси? Використовуй rest
- Rest завжди повертає масив, навіть якщо зайвих елементів немає (повертає
[])
Швидкий приклад
// Spread: розпаковує масив в окремі елементи
const nums = [1, 2, 3];
const more = [...nums, 4, 5]; // [1, 2, 3, 4, 5]
// Spread у виклику функції
function add(a, b, c) { return a + b + c; }
add(...nums); // 6
// Rest: збирає залишкові аргументи в масив
function sum(first, ...rest) {
return first + rest.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10, rest = [2, 3, 4]Контекст визначає, який оператор ти отримуєш. Ті самі три крапки, різні ролі.
Головна відмінність
Spread з'являється праворуч від операторів присвоєння або всередині викликів функцій. Він розбиває ітерабельне значення на частини і повертає окремі елементи. Rest з'являється ліворуч у параметрах функцій або у шаблонах деструктуризації (destructuring). Він збирає все, що залишилось, і загортає в масив. Один розширює, інший стискає.
Коли використовувати
- Скопіювати масив без мутації:
[...arr] - Об'єднати об'єкти:
{ ...obj1, ...obj2 } - Передати масив як окремі аргументи у функцію:
func(...arr) - Захопити змінну кількість аргументів функції:
function fn(a, ...rest) {} - Деструктурувати з залишковими пропсами:
const { x, ...rest } = obj
Таблиця порівняння
| Характеристика | Spread (...) | Rest (...) |
|---|---|---|
| Позиція | Праворуч (присвоєння, виклики) | Ліворуч (параметри, деструктуризація) |
| Ефект | Розгортає в окремі елементи | Збирає залишки в масив |
| У функціях | func(...arr) передає окремі аргументи | func(a, ...rest) збирає зайві аргументи |
| Масиви | [...arr1, ...arr2] об'єднує | const [first, ...rest] = arr розбиває |
| Об'єкти (ES2018+) | { ...obj1, ...obj2 } об'єднує | const { a, ...rest } = obj витягує решту |
| Коли використовувати | Копіювання, об'єднання, передача | Змінна кількість аргументів, залишкові пропси |
Як це працює
JavaScript-рушії обробляють ... як синтаксичний цукор: spread ітерує джерело через Symbol.iterator (для масивів) або Object.keys (для об'єктів), повертаючи значення по одному. Rest сканує список параметрів під час виклику і пакує хвостові аргументи в новий масив. Обидва створюють поверхневі копії, тому вкладені об'єкти спільно використовують посилання з оригіналом.
У React-проєктах spread зустрічається постійно. Шаблон { ...state, updatedField: newValue } - стандартний спосіб зберігати незмінність у редюсерах без зайвих бібліотек.
Типові помилки
Передача масиву у rest-функцію без spread:
function add(...nums) { return nums.reduce((a, b) => a + b); }
add([1, 2, 3]); // [1,2,3] стає першим аргументом, результат NaN
add(...[1, 2, 3]); // правильно: 6Припущення, що spread робить глибоку копію:
const a = { nested: { x: 1 } };
const b = { ...a };
a.nested.x = 99;
console.log(b.nested.x); // 99 - вкладений об'єкт спільний!
// Виправлення: розповсюджуй вкладений об'єкт теж
const c = { ...a, nested: { ...a.nested } };Розповсюдження не-ітерабельного значення:
[...42]; // TypeError: 42 is not iterable
[...'abc']; // працює: ['a', 'b', 'c'] - рядки є ітерабельними
[...new Set([1, 2, 2])]; // працює: [1, 2] - Set є ітерабельнимRest має бути останнім параметром:
function fn(a, ...rest, b) {} // SyntaxError
function fn(a, ...rest) {} // правильноРеальне використання
- React:
<Button {...buttonProps} />передає пропси в JSX - Redux редюсери:
{ ...state, count: state.count + 1 }для незмінних оновлень - Express middleware:
{ ...req.query, filter: 'active' }додає параметри запиту - Видалення дублікатів через Set:
[...new Set([1, 2, 2, 3])]повертає[1, 2, 3]
Питання для практики
Q: Що виведе const a = [1]; const b = [...a]; a.push(2); console.log(b);?
A: [1]. Spread створює новий масив, тому b не залежить від a.
Q: Що використовували до object spread (ES2018)?
A: Object.assign({}, obj1, obj2). Babel досі транспілює object spread до Object.assign для старих цільових платформ, тому результат однаковий.
Q: Що повертає rest, якщо зайвих аргументів немає?
A: Порожній масив []. Це зручніше за старий об'єкт arguments, який був схожий на масив, але масивом не був.
Q: Чи можна розповсюдити Set або Map?
A: Так, обидва є ітерабельними. [...new Set([1, 2, 2])] дає [1, 2]. Зручно для швидкого видалення дублікатів.
Q: Чому rest-параметри кращі за об'єкт arguments?
A: arguments схожий на масив, але не є ним, тому .map() і .reduce() не працюють з ним напряму. Rest повертає справжній масив. До того ж стрілкові функції взагалі не мають власного arguments.
Приклади
Об'єднання налаштувань користувача
const defaults = { theme: 'light', fontSize: 14, showSidebar: true };
const userPrefs = { fontSize: 18, language: 'en' };
// Пізніші ключі перекривають попередні
const settings = { ...defaults, ...userPrefs };
// { theme: 'light', fontSize: 18, showSidebar: true, language: 'en' }Порядок має значення. ...userPrefs стоїть другим, тому його fontSize перезаписує значення за замовчуванням. Цей шаблон стандартний для об'єднання конфігів та тем.
Логер зі змінною кількістю аргументів
function log(level, ...messages) {
const prefix = `[${level.toUpperCase()}]`;
console.log(prefix, ...messages); // знову розповсюджуємо messages
}
log('info', 'User logged in', 'userId:', 42);
// [INFO] User logged in userId: 42Rest збирає messages у масив. Потім spread передає їх назад як окремі аргументи в console.log. Rest збирає, spread розповсюджує.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.