Что такое операторы spread и rest?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#JavaScript #База JS

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

Операторы spread и rest — это синтаксис ES6, использующий три точки (...). Spread “разворачивает” элементы массива или свойства объекта, а rest “собирает” множественные элементы в массив или объект. Spread используется при вызове функций и создании новых структур данных, rest — в параметрах функций и деструктуризации.

Ключевые различия:

  • Spread — разворачивает ([...array], {...object})
  • Rest — собирает (function(...args), [first, ...rest])
  • Контекст — spread в правой части, rest в левой
  • Применение — копирование, объединение, передача параметров
  • Иммутабельность — создание новых структур без изменения исходных

Что такое операторы spread и rest

Операторы spread и rest — это мощные инструменты ES6, которые используют одинаковый синтаксис (...), но выполняют противоположные функции. Они упрощают работу с массивами, объектами и функциями, делая код более читаемым и функциональным.

Основное различие

// Spread — разворачивает элементы
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4, 5]; // [1, 2, 3, 4, 5]
 
// Rest — собирает элементы
function sum(...args) {
  return args.reduce((total, num) => total + num, 0);
}
 
console.log(sum(1, 2, 3, 4)); // 10

Оператор Spread (…)

1. Spread с массивами

Копирование массивов

const original = [1, 2, 3];
const copy = [...original];
 
console.log(copy); // [1, 2, 3]
console.log(original === copy); // false — разные объекты
 
// Традиционный способ
const oldCopy = original.slice();
// или
const oldCopy2 = Array.from(original);

Объединение массивов

const fruits = ['яблоко', 'банан'];
const vegetables = ['морковь', 'капуста'];
const food = [...fruits, ...vegetables];
 
console.log(food); // ['яблоко', 'банан', 'морковь', 'капуста']
 
// Добавление элементов
const numbers = [2, 3, 4];
const extended = [1, ...numbers, 5, 6];
console.log(extended); // [1, 2, 3, 4, 5, 6]
 
// Вставка в середину
const start = [1, 2];
const middle = [3, 4];
const end = [5, 6];
const combined = [...start, ...middle, ...end];
console.log(combined); // [1, 2, 3, 4, 5, 6]

Преобразование строки в массив

const word = 'привет';
const letters = [...word];
console.log(letters); // ['п', 'р', 'и', 'в', 'е', 'т']
 
// Работа с эмодзи
const emoji = '👨‍👩‍👧‍👦';
const emojiArray = [...emoji];
console.log(emojiArray); // Корректно разбивает составные эмодзи

2. Spread с объектами

Копирование объектов

const user = {
  name: 'Иван',
  age: 25,
  city: 'Москва'
};
 
const userCopy = {...user};
console.log(userCopy); // {name: 'Иван', age: 25, city: 'Москва'}
console.log(user === userCopy); // false
 
// Поверхностное копирование
const userWithAddress = {
  name: 'Мария',
  address: {street: 'Тверская', house: 10}
};
 
const copy = {...userWithAddress};
copy.address.house = 20; // Изменяет исходный объект!
console.log(userWithAddress.address.house); // 20

Объединение объектов

const basicInfo = {name: 'Петр', age: 30};
const contactInfo = {email: 'petr@example.com', phone: '+7-123-456-78-90'};
const fullInfo = {...basicInfo, ...contactInfo};
 
console.log(fullInfo);
// {name: 'Петр', age: 30, email: 'petr@example.com', phone: '+7-123-456-78-90'}
 
// Перезапись свойств
const defaults = {theme: 'light', language: 'ru', notifications: true};
const userSettings = {theme: 'dark', notifications: false};
const settings = {...defaults, ...userSettings};
 
console.log(settings);
// {theme: 'dark', language: 'ru', notifications: false}

Добавление новых свойств

const user = {name: 'Анна', age: 28};
const userWithId = {id: 1, ...user};
const userWithStatus = {...user, isActive: true, lastLogin: new Date()};
 
console.log(userWithId); // {id: 1, name: 'Анна', age: 28}
console.log(userWithStatus); // {name: 'Анна', age: 28, isActive: true, lastLogin: ...}

3. Spread в вызовах функций

// Передача элементов массива как отдельных аргументов
function multiply(a, b, c) {
  return a * b * c;
}
 
const numbers = [2, 3, 4];
console.log(multiply(...numbers)); // 24
 
// Поиск максимального значения
const scores = [85, 92, 78, 96, 88];
const maxScore = Math.max(...scores);
console.log(maxScore); // 96
 
// Добавление элементов в массив
const existingItems = ['item1', 'item2'];
const newItems = ['item3', 'item4'];
existingItems.push(...newItems);
console.log(existingItems); // ['item1', 'item2', 'item3', 'item4']

Оператор Rest (…)

1. Rest в параметрах функций

Функции с переменным количеством аргументов

// Сумма любого количества чисел
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
 
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum()); // 0
 
// Функция с обязательными и опциональными параметрами
function greet(greeting, ...names) {
  return `${greeting}, ${names.join(' и ')}!`;
}
 
console.log(greet('Привет', 'Иван')); // 'Привет, Иван!'
console.log(greet('Добро пожаловать', 'Мария', 'Петр')); // 'Добро пожаловать, Мария и Петр!'

Обработка аргументов

// Логирование с разными уровнями
function log(level, message, ...details) {
  console.log(`[${level.toUpperCase()}] ${message}`);
  if (details.length > 0) {
    console.log('Детали:', details);
  }
}
 
log('info', 'Пользователь вошел в систему');
log('error', 'Ошибка подключения', 'timeout', 'server unreachable');
 
// Создание обертки для функций
function withLogging(fn) {
  return function(...args) {
    console.log('Вызов функции с аргументами:', args);
    const result = fn(...args);
    console.log('Результат:', result);
    return result;
  };
}
 
const loggedSum = withLogging(sum);
console.log(loggedSum(1, 2, 3)); // Логирует вызов и результат

2. Rest в деструктуризации массивов

// Извлечение первого элемента и ;остальных
const numbers = [1, 2, 3, 4, 5];
const [first, ...rest] = numbers;
 
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
 
// Извлечение нескольких элементов
const colors = ['красный', 'зеленый', 'синий', 'желтый', 'фиолетовый'];
const [primary, secondary, ...others] = colors;
 
console.log(primary); // 'красный'
console.log(secondary); // 'зеленый'
console.log(others); // ['синий', 'желтый', 'фиолетовый']
 
// Получение последних элементов
function getLastElements(arr, count) {
  const [...allElements] = arr;
  return allElements.slice(-count);
}
 
console.log(getLastElements([1, 2, 3, 4, 5], 2)); // [4, 5]

3. Rest в деструктуризации объектов

// Извлечение определенных свойств
const user = {
  id: 1,
  name: 'Дмитрий',
  email: 'dmitry@example.com',
  age: 35,
  city: 'Санкт-Петербург',
  phone: '+7-987-654-32-10'
};
 
const {id, name, ...otherInfo} = user;
console.log(id); // 1
console.log(name); // 'Дмитрий'
console.log(otherInfo); // {email: '...', age: 35, city: '...', phone: '...'}
 
// Исключение чувствительных данных
const userWithPassword = {
  id: 2,
  username: 'maria_user',
  password: 'secret123',
  email: 'maria@example.com',
  role: 'admin'
};
 
const {password, ...safeUserData} = userWithPassword;
console.log(safeUserData); // Объект без пароля

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

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

// Обновление состояния с сохранением предыдущих значений
function updateUserProfile(currentUser, updates) {
  return {
    ...currentUser,
    ...updates,
    updatedAt: new Date()
  };
}
 
const user = {id: 1, name: 'Иван', email: 'ivan@example.com'};
const updatedUser = updateUserProfile(user, {name: 'Иван Петров'});
console.log(updatedUser);
// {id: 1, name: 'Иван Петров', email: 'ivan@example.com', updatedAt: ...}
 
// Добавление элемента в массив состояния
function addTodo(todos, newTodo) {
  return [...todos, {id: Date.now(), ...newTodo}];
}
 
const todos = [{id: 1, text: 'Купить молоко', done: false}];
const newTodos = addTodo(todos, {text: 'Выгулять собаку', done: false});
console.log(newTodos); // Новый массив с добавленной задачей

2. Работа с API

// Обработка ответа API
function processApiResponse(response) {
  const {data, meta, ...otherFields} = response;
  
  return {
    items: data.items || [],
    pagination: meta.pagination || {},
    additionalInfo: otherFields
  };
}
 
// Подготовка данных для отправки
function prepareUserData(formData, additionalFields = {}) {
  const {confirmPassword, ...userData} = formData;
  
  return {
    ...userData,
    ...additionalFields,
    createdAt: new Date().toISOString()
  };
}

3. Утилитарные функции

// Функция для объединения массивов без дубликатов
function uniqueConcat(...arrays) {
  const combined = [].concat(...arrays);
  return [...new Set(combined)];
}
 
const arr1 = [1, 2, 3];
const arr2 = [3, 4, 5];
const arr3 = [5, 6, 7];
console.log(uniqueConcat(arr1, arr2, arr3)); // [1, 2, 3, 4, 5, 6, 7]
 
// Функция для глубокого объединения объектов
function deepMerge(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();
 
  for (const key in source) {
    if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
      if (!target[key]) target[key] = {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
 
  return deepMerge(target, ...sources);
}

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

// Композиция функций
function compose(...functions) {
  return function(value) {
    return functions.reduceRight((acc, fn) => fn(acc), value);
  };
}
 
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
 
const composedFunction = compose(square, double, addOne);
console.log(composedFunction(3)); // ((3 + 1) * 2)² = 64
 
// Каррирование с rest параметрами
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return function(...nextArgs) {
      return curried(...args, ...nextArgs);
    };
  };
}
 
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6

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

АспектSpreadRest
Синтаксис...expression...parameter
ПозицияПравая часть выраженияЛевая часть, параметры
ФункцияРазворачивает элементыСобирает элементы
Массивы[...arr1, ...arr2][first, ...rest]
Объекты{...obj1, ...obj2}{prop, ...others}
Функцииfn(...args)function(...params)
РезультатНовая структура данныхМассив или объект
ПрименениеКопирование, объединениеСбор параметров

Продвинутые техники

1. Условный spread

// Условное добавление свойств
function createUser(name, email, isAdmin = false) {
  return {
    name,
    email,
    createdAt: new Date(),
    ...(isAdmin && {role: 'admin', permissions: ['read', 'write', 'delete']})
  };
}
 
console.log(createUser('Иван', 'ivan@example.com')); // Без роли
console.log(createUser('Мария', 'maria@example.com', true)); // С ролью admin
 
// Условное объединение массивов
function getMenuItems(isAuthenticated, isAdmin) {
  const baseItems = ['Главная', 'О нас', 'Контакты'];
  const userItems = ['Профиль', 'Настройки'];
  const adminItems = ['Панель управления', 'Пользователи'];
  
  return [
    ...baseItems,
    ...(isAuthenticated ? userItems : []),
    ...(isAdmin ? adminItems : [])
  ];
}

2. Spread с вычисляемыми свойствами

// Динамическое создание объектов
function createDynamicObject(key, value, additionalProps = {}) {
  return {
    id: Math.random(),
    timestamp: Date.now(),
    [key]: value,
    ...additionalProps
  };
}
 
const obj1 = createDynamicObject('username', 'john_doe', {email: 'john@example.com'});
const obj2 = createDynamicObject('productName', 'Laptop', {price: 999, category: 'Electronics'});

3. Spread для иммутабельных операций

// Иммутабельное обновление вложенных объектов
function updateNestedProperty(obj, path, value) {
  const [head, ...tail] = path;
  
  if (tail.length === 0) {
    return {...obj, [head]: value};
  }
  
  return {
    ...obj,
    [head]: updateNestedProperty(obj[head] || {}, tail, value)
  };
}
 
const state = {
  user: {
    profile: {
      settings: {
        theme: 'light'
      }
    }
  }
};
 
const newState = updateNestedProperty(state, ['user', 'profile', 'settings', 'theme'], 'dark');
console.log(newState.user.profile.settings.theme); // 'dark'
console.log(state.user.profile.settings.theme); // 'light' — исходный не изменился

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

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

// ❌ Избегайте spread в циклах для больших массивов
let result = [];
for (let i = 0; i < 10000; i++) {
  result = [...result, i]; // Создает новый массив каждый раз
}
 
// ✅ Используйте push для накопления
let result = [];
for (let i = 0; i < 10000; i++) {
  result.push(i);
}
 
// ✅ Или используйте spread для финального объединения
const chunks = [];
for (let i = 0; i < 100; i++) {
  chunks.push(Array.from({length: 100}, (_, j) => i * 100 + j));
}
const result = [].concat(...chunks);

2. Читаемость кода

// ❌ Слишком много spread операций
const result = {...obj1, ...obj2, ...obj3, ...obj4, ...obj5};
 
// ✅ Группируйте логически связанные операции
const baseConfig = {...defaultConfig, ...userConfig};
const finalConfig = {...baseConfig, ...environmentConfig};
 
// ❌ Неясное использование rest
function processData({a, b, c, d, e, f, ...rest}) {
  // Слишком много параметров
}
 
// ✅ Группируйте связанные параметры
function processData({userInfo, settings, ...metadata}) {
  const {name, email} = userInfo;
  const {theme, language} = settings;
  // Более понятная структура
}

3. Безопасность типов

// ❌ Небезопасное использование rest
function unsafeFunction(...args) {
  return args[0].toUpperCase(); // Ошибка если args[0] не строка
}
 
// ✅ Валидация параметров
function safeFunction(...args) {
  if (args.length === 0) {
    throw new Error('Требуется хотя бы один аргумент');
  }
  
  const [first] = args;
  if (typeof first !== 'string') {
    throw new Error('Первый аргумент должен быть строкой');
  }
  
  return first.toUpperCase();
}
 
// ✅ Использование значений по умолчанию
function createUser({name, email, ...options} = {}) {
  if (!name || !email) {
    throw new Error('Имя и email обязательны');
  }
  
  return {
    name,
    email,
    isActive: true,
    createdAt: new Date(),
    ...options
  };
}

Современные возможности

1. Spread с опциональной цепочкой

// Безопасное разворачивание потенциально undefined значений
const user = {
  name: 'Иван',
  preferences: null
};
 
const settings = {
  theme: 'light',
  ...user.preferences, // Ошибка!
};
 
// ✅ Безопасная версия
const safeSettings = {
  theme: 'light',
  ...(user.preferences ?? {})
};
 
// ✅ С опциональной цепочкой
const config = {
  ...defaultConfig,
  ...user?.preferences,
  ...user?.settings?.ui
};

2. Spread с приватными полями

// Создание публичного API из объекта с приватными данными
function createPublicUser(userData) {
  const {password, internalId, ...publicData} = userData;
  
  return {
    ...publicData,
    id: internalId,
    hasPassword: Boolean(password)
  };
}

3. Spread в TypeScript

// Типизированный spread
interface User {
  id: number;
  name: string;
  email: string;
}
 
interface UserUpdate {
  name?: string;
  email?: string;
}
 
function updateUser(user: User, updates: UserUpdate): User {
  return {
    ...user,
    ...updates
  };
}
 
// Rest с типизацией
function logMessages(level: string, ...messages: string[]): void {
  console.log(`[${level}]`, ...messages);
}

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

Задача 1: Объединение массивов

Условие: Создайте функцию, которая принимает любое количество массивов и возвращает новый массив со всеми уникальными элементами.

function mergeUnique(...arrays) {
  // Ваш код здесь
}
 
console.log(mergeUnique([1, 2], [2, 3], [3, 4])); // [1, 2, 3, 4]
console.log(mergeUnique(['a', 'b'], ['b', 'c'], ['c', 'd'])); // ['a', 'b', 'c', 'd']
Решение
function mergeUnique(...arrays) {
  const combined = [].concat(...arrays);
  return [...new Set(combined)];
}
 
// Альтернативное решение
function mergeUnique(...arrays) {
  return [...new Set(arrays.flat())];
}

Задача 2: Обновление объекта

Условие: Создайте функцию для обновления пользователя, которая принимает исходный объект пользователя и объект с обновлениями, возвращая новый объект с обновленными данными и временной меткой.

function updateUserWithTimestamp(user, updates) {
  // Ваш код здесь
  // Должна добавлять поле updatedAt с текущей датой
}
 
const user = {id: 1, name: 'Иван', email: 'ivan@example.com'};
const updates = {name: 'Иван Петров', city: 'Москва'};
 
const result = updateUserWithTimestamp(user, updates);
console.log(result);
// {id: 1, name: 'Иван Петров', email: 'ivan@example.com', city: 'Москва', updatedAt: ...}
Решение
function updateUserWithTimestamp(user, updates) {
  return {
    ...user,
    ...updates,
    updatedAt: new Date()
  };
}

Задача 3: Функция с опциональными параметрами

Условие: Создайте функцию для создания HTML элемента, которая принимает тег, содержимое и любое количество атрибутов.

function createElement(tag, content, ...attributes) {
  // Ваш код здесь
  // attributes — массив объектов вида {name: 'class', value: 'button'}
}
 
console.log(createElement('div', 'Привет мир'));
// '<div>Привет мир</div>'
 
console.log(createElement('button', 'Нажми меня', 
  {name: 'class', value: 'btn'}, 
  {name: 'id', value: 'submit-btn'}
));
// '<button class="btn" id="submit-btn">Нажми меня</button>'
Решение
function createElement(tag, content, ...attributes) {
  const attrs = attributes
    .map(attr => `${attr.name}="${attr.value}"`)
    .join(' ');
  
  const attrString = attrs ? ` ${attrs}` : '';
  return `<${tag}${attrString}>${content}</${tag}>`;
}

Заключение

Операторы spread и rest — это мощные инструменты современного JavaScript, которые значительно упрощают работу с массивами, объектами и функциями. Они способствуют написанию более чистого, читаемого и функционального кода.

Ключевые преимущества:

  • Иммутабельность — создание новых структур без изменения исходных
  • Гибкость — работа с переменным количеством параметров
  • Читаемость — более понятный и лаконичный синтаксис
  • Функциональность — поддержка функционального стиля программирования

Понимание различий между spread и rest, а также знание лучших практик их использования, поможет вам писать более эффективный и поддерживаемый код.


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