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

const sum = (a, b) => a + b;

sum(1, 2);   // 3
sum(-3, 10); // 7

Пока всё просто и понятно. Сложности возникают при дополнительных требованиях: что, если захотим суммировать не два, а три числа? Или пять, или даже десять? Писать для обработки каждого случая отдельную функцию — очевидно плохой, непрактичный вариант:

const sumOfTwo = (a, b) => a + b;
const sumOfTree = (a, b, c) => a + b + c;
const sumOfTen = (a, b, c, d, e, f, g, h, i, j) => a + b + c + d + e + f + g + h + i + j; // фух...
// const sumoOfThousand = ???
// const sumOfMillion = ???

Надо, чтобы единая функция могла работать с разным количеством аргументов. Как это сделать?

Можно заметить, что в стандартной библиотеке JavaScript существуют функции, которые могут принимать разное количество аргументов. Например, сигнатура функции Math.max определяется так:

Math.max([value1[, value2[, ...]]])

Она говорит нам о том, что в Math.max можно передать любое количество элементов:

Math.max(10, 20);             // 20
Math.max(10, 20, 30);         // 30
Math.max(10, 20, 30, 40, 50); // 50
Math.max(-10, -20, -30);      // -10

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

const func = (...params) => {
  // params — это массив, содержащий все
  // переданные при вызове функции аргументы
  console.log(params);
};

func();            // => []
func(9);           // => [9]
func(9, 4);        // => [9, 4]
func(9, 4, 1);     // => [9, 4, 1]
func(9, 4, 1, -3); // => [9, 4, 1, -3]

Символ троеточия ... перед именем формального параметра в определении функции обозначает rest-оператор. Запись ...params в определении func из примера выше означает буквально следующее: "все переданные при вызове функции аргументы поместить в массив params".

Если вовсе не передать аргументов, то rest-массив params будет пустым:

func(); // => []

В функцию можно передать любое количество аргументов — все они попадут в rest-массив params:

func(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Аргументы могут быть любого типа — числа, строки, массивы и др.:

func(1, 2, 'hello', [3, 4, 5], true);
// => [1, 2, 'hello', [3, 4, 5 ], true]

Основная сложность для понимания оператора ... состоит в том, что он выполняет различные действия в зависимости от того, где применяется. В определении функции он выполняет "упаковку" параметров, а при вызове — наоборот, "распаковку". Упаковке параметров посвящён этот урок и вы уже видели, как это делается. В следующем же уроке рассмотрим ... в роли распаковщика (spread-оператор).

Теперь у нас достаточно знаний, чтобы с помощью rest-оператора переписать нашу фунцию sum так, чтобы она умела суммировать любое количество чисел (а не только два числа, как сейчас):

const sum = (...numbers) => {
  let result = 0;
  for (let num of numbers) {
    result += num;
  }
  return result;
};

sum();         // 0
sum(10);       // 10
sum(10, 4);    // 14
sum(8, 10, 4); // 22

Наша реализация sum имеет интересную особенность: мы решили сделать так, что при вызове без аргументов возвращается значение 0 ("Ничего"), при вызове с одним число возвращается это же число. Хотя можно было обработать эти ситуацию как-то по-другому (например, вернуть специальное значение null), это вопрос удобного выбора, подходящего для решения конкретных практических задач.

В таком контексте rest-массив можно считать, как "необязательные аргументы", которые можно либо вовсе не передавать либо передавать столько, сколько хочешь. А что, если мы захотим, чтобы функция имела два обыкновенных ("обязательных") именованных параметра, а остальные были необязательными и сохранялись в rest-массиве? Всё просто: при определении функции сначала указываем стандартные именованные формальные параметры (например, a и b) и в конце добавляем rest-массив:

const func = (a, b, ...params) => {
  // параметр 'a' содержит первый аргумент
  console.log(`a -> ${a}`);
  // параметр 'b' содержит второй аргумент
  console.log(`b -> ${b}`);
  // params содержит все остальные аргументы
  console.log(params);
};

func(9, 4);
// => a -> 9
// => b -> 4
// => []
func(9, 4, 1);
// => a -> 9
// => b -> 4
// => [1]
func(9, 4, 1, -3);
// => a -> 9
// => b -> 4
// => [1, -3]
func(9, 4, 1, -3, -5);
// => a -> 9
// => b -> 4
// => [1, -3, -5]

То же можно сделать и для одного аргумента:

const func = (a, ...params) => {
  // ...
};

и для трёх:

const func = (a, b, c, ...params) => {
  // ...
};

Эту идею можно продолжать и дальше, делая обязательными то количество аргументов, которое требуется. Единственное ограничение: rest-оператор может быть использован только для последнего параметра. То есть такой код синтаксически не верен:

const func = (...params, a) => {
  // ...
};

И такой тоже:

const func = (a, ...params, b) => {
  // ...
};

Именно поэтому оператор называется rest, то есть он организует хранение "остальных" ("оставшихся", последних) параметров.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →