← JavaScript/Rest и Spread#76 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Rest и Spread

Реальная проблема: функции с переменным числом аргументов

В lodash функция _.merge(obj, ...sources) принимает любое количество объектов для слияния. В React clsx('btn', isActive && 'btn-active', className) принимает любое число классов. Синтаксис ... делает эти API возможными.

Что решают Rest и Spread

Оба используют ..., но делают противоположное:

  • Rest — собирает несколько значений в массив (в параметрах функции)
  • Spread — разворачивает массив/объект в отдельные значения (в вызовах и литералах)
  • На основе предыдущих уроков

  • «Функции» — параметры функций, arguments (устаревший способ)
  • «Массивы» — spread для создания копий, слияния
  • «Объекты» — spread для слияния объектов
  • «Деструктуризация» — ...rest в деструктуризации — тот же синтаксис
  • Rest — собирает аргументы в массив

    Должен быть последним параметром. Заменяет устаревший arguments:

    function sum(...numbers) {
      // numbers — обычный массив со всеми аргументами
      return numbers.reduce((acc, n) => acc + n, 0)
    }
    
    console.log(sum(1, 2, 3, 4, 5))  // 15
    console.log(sum(10, 20))          // 30
    console.log(sum())                // 0
    
    // Фиксированные параметры + остаток:
    function log(level, timestamp, ...messages) {
      console.log(`[${level}] ${timestamp}:`, ...messages)
    }
    
    log('INFO', '10:30', 'Запрос', 'GET /api/users', '200 OK')
    // [INFO] 10:30: Запрос GET /api/users 200 OK

    Spread — разворачивает в отдельные значения

    В вызове функции:

    const nums = [3, 1, 4, 1, 5, 9, 2, 6]
    Math.max(...nums)   // 9 — как Math.max(3, 1, 4, 1, 5, 9, 2, 6)
    Math.min(...nums)   // 1
    
    // Строка тоже iterable:
    console.log([...'hello'])  // ['h', 'e', 'l', 'l', 'o']

    Копирование и слияние массивов:

    const fruits  = ['яблоко', 'банан']
    const veggies = ['морковь', 'лук']
    
    const all    = [...fruits, ...veggies]           // ['яблоко', 'банан', 'морковь', 'лук']
    const copy   = [...fruits]                       // поверхностная копия
    const mixed  = ['авокадо', ...fruits, 'дыня']   // вставка в середину

    Слияние объектов (правые ключи перезаписывают левые):

    const defaults    = { theme: 'dark', lang: 'ru', fontSize: 14, notifications: true }
    const userPrefs   = { theme: 'light', notifications: false }
    const config      = { ...defaults, ...userPrefs }
    // { theme: 'light', lang: 'ru', fontSize: 14, notifications: false }
    
    // Обновление одного поля — иммутабельно:
    const newConfig = { ...config, lang: 'en' }

    Spread не делает глубокую копию:

    const user = { name: 'Иван', address: { city: 'Москва' } }
    const copy = { ...user }
    copy.address.city = 'СПб'   // изменит и user.address.city!
    // Для глубокой копии: structuredClone(user) или вложенный spread

    Типичные ошибки

    Ошибка 1: rest не последний параметр

    // Сломано — SyntaxError:
    function bad(a, ...rest, b) { }
    
    // Исправлено — rest только последний:
    function good(a, b, ...rest) { }

    Ошибка 2: spread не-итерируемого

    // Сломано:
    const obj = { a: 1 }
    console.log([...obj])  // TypeError: obj is not iterable
    
    // Spread объекта работает только в объектном контексте:
    const copy = { ...obj }  // OK

    Ошибка 3: spread vs concat

    // Оба делают одно, но spread нагляднее:
    const merged1 = arr1.concat(arr2)     // старый способ
    const merged2 = [...arr1, ...arr2]    // современный
    
    // Но при большом количестве массивов concat эффективнее:
    const mergedMany = [].concat(...manyArrays)

    В реальных проектах

  • React: <Component {...props} /> — передача всех пропсов
  • Redux: return { ...state, loading: true } — иммутабельное обновление state
  • API-утилиты: function request(url, { method = 'GET', ...options } = {})
  • Слияние конфигов: { ...defaultConfig, ...envConfig, ...userConfig }
  • Вариативные функции: логгеры, событийные системы
  • Примеры

    Утилиты для работы с данными: pick, omit, merge

    // Утилита pick — выбрать только нужные ключи
    function pick(obj, ...keys) {
      return keys.reduce((result, key) => {
        if (key in obj) result[key] = obj[key]
        return result
      }, {})
    }
    
    // Утилита omit — исключить ключи
    function omit(obj, ...keys) {
      const excluded = new Set(keys)
      return Object.fromEntries(
        Object.entries(obj).filter(([k]) => !excluded.has(k))
      )
    }
    
    // Глубокое слияние (merge)
    function mergeDeep(...objects) {
      return objects.reduce((result, obj) => {
        for (const [key, value] of Object.entries(obj)) {
          if (value && typeof value === 'object' && !Array.isArray(value)) {
            result[key] = mergeDeep(result[key] ?? {}, value)
          } else {
            result[key] = value
          }
        }
        return result
      }, {})
    }
    
    const user = {
      id: 1,
      name: 'Алексей',
      email: 'alex@mail.ru',
      password: 'hashed_secret',
      role: 'admin',
      settings: { theme: 'dark', notifications: true }
    }
    
    // Безопасный объект для API-ответа (без пароля)
    const safeUser = omit(user, 'password')
    console.log(safeUser)
    // { id: 1, name: 'Алексей', email: 'alex@mail.ru', role: 'admin', settings: {...} }
    
    // Только публичные поля
    const publicUser = pick(user, 'id', 'name', 'role')
    console.log(publicUser)  // { id: 1, name: 'Алексей', role: 'admin' }
    
    // Глубокое слияние настроек
    const defaults = { theme: 'dark', editor: { tabSize: 2, wrap: false } }
    const overrides = { editor: { tabSize: 4, spell: true } }
    const merged = mergeDeep(defaults, overrides)
    console.log(merged)
    // { theme: 'dark', editor: { tabSize: 4, wrap: false, spell: true } }
    
    // Вариативная функция логгера
    function logger(level, ...messages) {
      const timestamp = new Date().toISOString().slice(11, 19)
      const text = messages.map(m =>
        typeof m === 'object' ? JSON.stringify(m) : String(m)
      ).join(' ')
      console.log(`[${timestamp}] [${level.toUpperCase()}] ${text}`)
    }
    
    logger('info', 'Пользователь', { id: 1 }, 'вошёл в систему')
    // [10:30:45] [INFO] Пользователь {"id":1} вошёл в систему

    Rest и Spread

    Реальная проблема: функции с переменным числом аргументов

    В lodash функция _.merge(obj, ...sources) принимает любое количество объектов для слияния. В React clsx('btn', isActive && 'btn-active', className) принимает любое число классов. Синтаксис ... делает эти API возможными.

    Что решают Rest и Spread

    Оба используют ..., но делают противоположное:

  • Rest — собирает несколько значений в массив (в параметрах функции)
  • Spread — разворачивает массив/объект в отдельные значения (в вызовах и литералах)
  • На основе предыдущих уроков

  • «Функции» — параметры функций, arguments (устаревший способ)
  • «Массивы» — spread для создания копий, слияния
  • «Объекты» — spread для слияния объектов
  • «Деструктуризация» — ...rest в деструктуризации — тот же синтаксис
  • Rest — собирает аргументы в массив

    Должен быть последним параметром. Заменяет устаревший arguments:

    function sum(...numbers) {
      // numbers — обычный массив со всеми аргументами
      return numbers.reduce((acc, n) => acc + n, 0)
    }
    
    console.log(sum(1, 2, 3, 4, 5))  // 15
    console.log(sum(10, 20))          // 30
    console.log(sum())                // 0
    
    // Фиксированные параметры + остаток:
    function log(level, timestamp, ...messages) {
      console.log(`[${level}] ${timestamp}:`, ...messages)
    }
    
    log('INFO', '10:30', 'Запрос', 'GET /api/users', '200 OK')
    // [INFO] 10:30: Запрос GET /api/users 200 OK

    Spread — разворачивает в отдельные значения

    В вызове функции:

    const nums = [3, 1, 4, 1, 5, 9, 2, 6]
    Math.max(...nums)   // 9 — как Math.max(3, 1, 4, 1, 5, 9, 2, 6)
    Math.min(...nums)   // 1
    
    // Строка тоже iterable:
    console.log([...'hello'])  // ['h', 'e', 'l', 'l', 'o']

    Копирование и слияние массивов:

    const fruits  = ['яблоко', 'банан']
    const veggies = ['морковь', 'лук']
    
    const all    = [...fruits, ...veggies]           // ['яблоко', 'банан', 'морковь', 'лук']
    const copy   = [...fruits]                       // поверхностная копия
    const mixed  = ['авокадо', ...fruits, 'дыня']   // вставка в середину

    Слияние объектов (правые ключи перезаписывают левые):

    const defaults    = { theme: 'dark', lang: 'ru', fontSize: 14, notifications: true }
    const userPrefs   = { theme: 'light', notifications: false }
    const config      = { ...defaults, ...userPrefs }
    // { theme: 'light', lang: 'ru', fontSize: 14, notifications: false }
    
    // Обновление одного поля — иммутабельно:
    const newConfig = { ...config, lang: 'en' }

    Spread не делает глубокую копию:

    const user = { name: 'Иван', address: { city: 'Москва' } }
    const copy = { ...user }
    copy.address.city = 'СПб'   // изменит и user.address.city!
    // Для глубокой копии: structuredClone(user) или вложенный spread

    Типичные ошибки

    Ошибка 1: rest не последний параметр

    // Сломано — SyntaxError:
    function bad(a, ...rest, b) { }
    
    // Исправлено — rest только последний:
    function good(a, b, ...rest) { }

    Ошибка 2: spread не-итерируемого

    // Сломано:
    const obj = { a: 1 }
    console.log([...obj])  // TypeError: obj is not iterable
    
    // Spread объекта работает только в объектном контексте:
    const copy = { ...obj }  // OK

    Ошибка 3: spread vs concat

    // Оба делают одно, но spread нагляднее:
    const merged1 = arr1.concat(arr2)     // старый способ
    const merged2 = [...arr1, ...arr2]    // современный
    
    // Но при большом количестве массивов concat эффективнее:
    const mergedMany = [].concat(...manyArrays)

    В реальных проектах

  • React: <Component {...props} /> — передача всех пропсов
  • Redux: return { ...state, loading: true } — иммутабельное обновление state
  • API-утилиты: function request(url, { method = 'GET', ...options } = {})
  • Слияние конфигов: { ...defaultConfig, ...envConfig, ...userConfig }
  • Вариативные функции: логгеры, событийные системы
  • Примеры

    Утилиты для работы с данными: pick, omit, merge

    // Утилита pick — выбрать только нужные ключи
    function pick(obj, ...keys) {
      return keys.reduce((result, key) => {
        if (key in obj) result[key] = obj[key]
        return result
      }, {})
    }
    
    // Утилита omit — исключить ключи
    function omit(obj, ...keys) {
      const excluded = new Set(keys)
      return Object.fromEntries(
        Object.entries(obj).filter(([k]) => !excluded.has(k))
      )
    }
    
    // Глубокое слияние (merge)
    function mergeDeep(...objects) {
      return objects.reduce((result, obj) => {
        for (const [key, value] of Object.entries(obj)) {
          if (value && typeof value === 'object' && !Array.isArray(value)) {
            result[key] = mergeDeep(result[key] ?? {}, value)
          } else {
            result[key] = value
          }
        }
        return result
      }, {})
    }
    
    const user = {
      id: 1,
      name: 'Алексей',
      email: 'alex@mail.ru',
      password: 'hashed_secret',
      role: 'admin',
      settings: { theme: 'dark', notifications: true }
    }
    
    // Безопасный объект для API-ответа (без пароля)
    const safeUser = omit(user, 'password')
    console.log(safeUser)
    // { id: 1, name: 'Алексей', email: 'alex@mail.ru', role: 'admin', settings: {...} }
    
    // Только публичные поля
    const publicUser = pick(user, 'id', 'name', 'role')
    console.log(publicUser)  // { id: 1, name: 'Алексей', role: 'admin' }
    
    // Глубокое слияние настроек
    const defaults = { theme: 'dark', editor: { tabSize: 2, wrap: false } }
    const overrides = { editor: { tabSize: 4, spell: true } }
    const merged = mergeDeep(defaults, overrides)
    console.log(merged)
    // { theme: 'dark', editor: { tabSize: 4, wrap: false, spell: true } }
    
    // Вариативная функция логгера
    function logger(level, ...messages) {
      const timestamp = new Date().toISOString().slice(11, 19)
      const text = messages.map(m =>
        typeof m === 'object' ? JSON.stringify(m) : String(m)
      ).join(' ')
      console.log(`[${timestamp}] [${level.toUpperCase()}] ${text}`)
    }
    
    logger('info', 'Пользователь', { id: 1 }, 'вошёл в систему')
    // [10:30:45] [INFO] Пользователь {"id":1} вошёл в систему

    Задание

    Напиши функцию pipeline(...fns), которая принимает любое количество функций-преобразователей и возвращает новую функцию. Эта новая функция принимает начальное значение и последовательно применяет к нему все переданные функции (результат каждой передаётся следующей). Это паттерн «конвейер» (pipe).

    Подсказка

    fns.reduce((value, fn) => fn(value), value) — начни с начального значения и применяй каждую функцию по очереди.

    Загружаем среду выполнения...
    Загружаем AI-помощника...