Spread и rest в JavaScript: три точки

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».

Оператор из трёх точек ... — один из самых полезных в ECMAScript 6. Ловушка новичка в том, что он делает два противоположных дела в зависимости от места. Разберёмся раз и навсегда.

Spread против rest: одно правило

Три точки: spread разворачивает, rest собирает

SPREAD — разворачивает

В вызове функции или литерале

  • fn(...arr) — передать элементы
  • [...a, ...b] — собрать массив
  • { ...obj } — скопировать объект
  • Берёт одно → раскладывает на много

REST — собирает

В параметрах или деструктуризации

  • function f(...args) — все аргументы
  • const [a, ...rest] = arr
  • const { x, ...rest } = obj
  • Берёт много → собирает в один массив/объект

Место решает всё. Если ... стоит там, где создаётся что-то (вызов, литерал массива/объекта) — это spread, он разворачивает. Если там, где что-то принимается (параметры функции, левая часть деструктуризации) — это rest, он собирает. Синтаксис один, направление противоположное.

Spread: копирование и слияние

const a = [1, 2];
const b = [3, 4];

const merged = [...a, ...b];        // [1, 2, 3, 4] — слить
const copy = [...a];                // [1, 2] — поверхностная копия
const withExtra = [0, ...a, 3];     // [0, 1, 2, 3] — вставить в середину

// объекты
const base = { name: 'Аня', role: 'user' };
const updated = { ...base, role: 'admin' };   // { name: 'Аня', role: 'admin' }

Slice-копия одной строкой и иммутабельное обновление — главные применения. { ...base, role: 'admin' } создаёт новый объект с изменённым полем, не трогая исходный. Так обновляют состояние в React и Redux: не мутируют, а создают новую версию.

При совпадении ключей побеждает правый — поэтому role: 'admin' перекрывает role: 'user' из base. Порядок важен.

Ловушка: копия поверхностная

const user = { name: 'Аня', address: { city: 'Москва' } };
const copy = { ...user };

copy.name = 'Борис';           // ✅ оригинал цел
copy.address.city = 'Казань';  // ❌ address ОБЩИЙ — оригинал изменится!

... копирует только верхний уровень. Вложенные объекты и массивы остаются общими ссылками — это природа ссылочных типов, а не баг spread. Для настоящей независимости — structuredClone(user). Ошибка на этом месте — классика при работе с состоянием.

Spread в вызове функции

const nums = [5, 2, 8, 1];

Math.max(...nums);          // 8 — развернуть массив в аргументы
console.log(...nums);       // 5 2 8 1

// объединить массивы через push
const target = [1, 2];
target.push(...[3, 4]);     // target станет [1, 2, 3, 4]

Math.max принимает отдельные аргументы, а не массив — spread разворачивает nums в Math.max(5, 2, 8, 1). Это заменило старый неуклюжий Math.max.apply(null, nums).

Spread работает с любым итерируемым: массивом, строкой, Set, Map:

[...'abc'];                 // ['a', 'b', 'c'] — строка по символам
[...new Set([1, 1, 2])];    // [1, 2] — убрать дубли через Set

Трюк [...new Set(arr)] — самый короткий способ убрать повторы из массива.

Rest: собрать аргументы

function sum(...nums) {           // все аргументы → настоящий массив
  return nums.reduce((a, b) => a + b, 0);
}

sum(1, 2, 3);        // 6
sum(1, 2, 3, 4, 5);  // 15 — сколько угодно

Rest в параметрах собирает произвольное число аргументов в массив. В отличие от старого объекта arguments, nums — настоящий массив, к нему сразу применимы reduce, map, filter.

Комбинируется с обычными параметрами — rest всегда последний:

function log(level, ...messages) {   // level — первый, остальное в messages
  console.log(`[${level}]`, ...messages);
}
log('info', 'старт', 'сервера');     // [info] старт сервера

Rest в деструктуризации

const [first, ...others] = [1, 2, 3, 4];
// first = 1, others = [2, 3, 4]

const { password, ...safe } = { name: 'Аня', password: '123', role: 'user' };
// safe = { name: 'Аня', role: 'user' } — без пароля

const { password, ...safe } — идиома «убрать одно поле». Вынимаем password, всё остальное собираем в safe — удобно, чтобы отдать объект наружу без чувствительного поля. Подробнее про разбор — в статье о деструктуризации.

rest против arguments

// ❌ старый способ
function oldSum() {
  const args = Array.prototype.slice.call(arguments);  // arguments — не массив!
  return args.reduce((a, b) => a + b, 0);
}

// ✅ современный
function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}

Старый arguments — псевдомассив: у него есть length, но нет методов массива, и в стрелочных функциях его вообще нет. Rest решает обе проблемы: настоящий массив, работает везде. Новый код arguments не использует.


Смежные темы: деструктуризация — где rest тоже живёт; примитивы и ссылки — почему копия поверхностная; методы массива. Полный список — в уроках JavaScript.

Частые вопросы

Чем spread отличается от rest, если синтаксис одинаковый?
По месту: spread разворачивает — стоит в вызове функции или литерале ([…arr], {…obj}). Rest собирает — стоит в объявлении параметров или при деструктуризации (function f(…args)). Один разбирает, другой собирает.
Как слить два массива или объекта?
Массивы: […a, …b]. Объекты: { …a, …b } — при совпадении ключей побеждает правый. Это создаёт новый массив/объект, не меняя исходные.
spread делает глубокую копию?
Нет, только поверхностную. […arr] и { …obj } копируют верхний уровень, вложенные объекты остаются общими. Для глубокой копии нужен structuredClone.
Зачем rest в параметрах функции?
Чтобы принять произвольное число аргументов в виде настоящего массива: function sum(…nums). Это современная замена устаревшему объекту arguments, который массивом не является.
Можно ли использовать spread со строкой?
Да, строка итерируема: […‘abc’] даёт [‘a’,‘b’,‘c’]. Работает с любым итерируемым — массивом, строкой, Set, Map, результатом генератора.