JavaScript Оператор Rest Упаковка аргументов
Давайте попробуем реализовать очень простую функцию, суммирующую числа. Для начала определим функцию 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
, то есть он организует хранение "остальных" ("оставшихся", последних) параметров.