JS: Funciones

Teoría: Operador Rest (empaquetar argumentos)

Vamos a intentar implementar una función muy simple que sume números. Para empezar, definamos la función sum(), que toma dos números como entrada y devuelve su suma:

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

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

Hasta aquí todo es simple y claro. Sin embargo, surgen dificultades cuando se plantean requisitos adicionales: ¿qué pasa si queremos sumar no dos, sino tres números? ¿O cinco, o incluso diez? Es obviamente una mala opción escribir una función separada para cada caso:

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; // uff...
// const sumOfThousand = ???
// const sumOfMillion = ???

Necesitamos que una única función pueda trabajar con diferentes cantidades de argumentos. ¿Cómo podemos lograr esto?

Podemos observar que en la biblioteca estándar de JavaScript existen funciones que pueden tomar diferentes cantidades de argumentos. Por ejemplo, la firma de la función Math.max() se define de la siguiente manera:

Math.max([valor1[, valor2[, ...]]])

Esto nos dice que en Math.max() se pueden pasar cualquier cantidad de elementos y que no son obligatorios:

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

Desde el punto de vista de la llamada, no hay nada inusual, simplemente se pasan diferentes números de argumentos. Sin embargo, la definición de una función con un número variable de argumentos se ve inusual:

const func = (...params) => {
  // params es un array que contiene todos los argumentos
  // pasados a la función en la llamada
  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]

El símbolo de tres puntos ... antes del nombre del parámetro formal en la definición de la función se llama operador rest. La expresión ...params en la definición de func() del ejemplo anterior significa literalmente lo siguiente: "coloca todos los argumentos pasados en la llamada de la función en el array params".

Si no se pasan argumentos, el array params estará vacío:

func(); // => []

Se pueden pasar cualquier cantidad de argumentos a la función, y todos ellos se almacenarán en el array params:

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

Los argumentos pueden ser de cualquier tipo: números, cadenas, arrays, etc.:

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

Ahora tenemos suficiente conocimiento para reescribir nuestra función sum() utilizando el operador rest, de modo que pueda sumar cualquier cantidad de números (no solo dos, como hasta ahora):

const sum = (...numbers) => {
  // Por defecto es 0, ya que la suma de nada es 0
  let result = 0;
  for (const num of numbers) {
    result += num;
  }
  return result;
};

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

En este contexto, se puede considerar que el array es un "argumento opcional" que se puede omitir por completo o pasar tantos como se desee. Y si queremos que la función tenga dos parámetros nombrados "obligatorios" y los demás sean opcionales y se almacenen en un array rest, simplemente especificamos los parámetros nombrados formales estándar primero (por ejemplo, a y b) y al final agregamos el array rest:

const func = (a, b, ...params) => {
  // el parámetro 'a' contiene el primer argumento
  console.log(`a -> ${a}`);
  // el parámetro 'b' contiene el segundo argumento
  console.log(`b -> ${b}`);
  // params contiene todos los demás argumentos
  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]

Lo mismo se puede hacer para un solo argumento:

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

y para tres:

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

Esta idea se puede seguir extendiendo, haciendo obligatorio el número de argumentos que se requiera. La única limitación es que el operador rest solo se puede usar para el último parámetro. Es decir, este código es sintácticamente incorrecto:

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

Y este también:

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

Es por eso que el operador se llama rest, ya que organiza el almacenamiento de los parámetros "restantes" (últimos).

Completado

0 / 16