Módulo 00 // Unidad 03

Funciones como valores

Declarar funciones de tres formas y tratarlas como un dato más

Unidad para alumnos del curso

Comprobando tu sesión…

Inicia sesión con la cuenta de Google con la que estás matriculado para ver esta unidad.

Esa cuenta no tiene acceso a este curso. Prueba con otra.

No se ha podido cargar el inicio de sesión de Google. Comprueba la conexión o si algún bloqueador lo impide, y recarga la página.

En Java, un método pertenece a una clase y sólo existe dentro de ella. En JavaScript, una función es un valor, igual que un número o un texto: se guarda en variables, se pasa a otras funciones y se devuelve como resultado. Casi todo lo que harás en el navegador (eventos, peticiones, métodos de array) se apoya en esa idea.

1 // Tres formas de escribir una función

La misma función se puede escribir de tres maneras:

// 1. Declaración de función
function sumar(a, b) {
  return a + b;
}

// 2. Expresión de función: una función sin nombre guardada en una constante
const restar = function (a, b) {
  return a - b;
};

// 3. Función flecha
const multiplicar = (a, b) => a * b;

Una función flecha (arrow function) es una forma abreviada de escribir una expresión de función. Toma su nombre del símbolo =>, que separa las dos mitades: a la izquierda, los parámetros que recibe; a la derecha, el cuerpo, lo que hace o devuelve. No lleva la palabra function ni un nombre propio, así que para usarla se guarda en una constante o se pasa directamente como argumento a otra función. Son las más habituales en código moderno, sobre todo para funciones cortas.

LAS PARTES DE UNA FUNCIÓN FLECHA cuerpo de una sola expresión const multiplicar = (a, b) => a * b la guarda en una constante parámetros flecha cuerpo: se devuelve return implícito equivale a function (a, b) { return a * b; } cuerpo con llaves const multiplicar = (a, b) => { const r = a * b; return r; } cuerpo: un bloque de código sin return devuelve undefined
Fíjate en lo que hay a la derecha de la flecha: si es una expresión suelta, se devuelve sola; si abre llaves, es un bloque de código y hay que escribir return.

La mejor forma de entenderlas es convertir una función normal paso a paso. Cada bloque es una versión alternativa de la misma función, no un programa seguido:

// 1. Punto de partida: una declaración de función
function doble(n) {
  return n * 2;
}

// 2. Se guarda en una constante como expresión de función
const doble = function (n) {
  return n * 2;
};

// 3. Se quita la palabra function y se escribe => entre parámetros y cuerpo
const doble = (n) => {
  return n * 2;
};

// 4. El cuerpo es una sola expresión: fuera las llaves y el return
const doble = (n) => n * 2;

// 5. Hay un único parámetro: los paréntesis se pueden quitar
const doble = n => n * 2;

Donde más se nota el ahorro es al pasar una función como argumento, que es lo que harás a diario. setTimeout recibe la función que debe ejecutar pasado un tiempo:

// Con una expresión de función
setTimeout(function () {
  console.log('Han pasado dos segundos');
}, 2000);

// Con una función flecha: sin parámetros, paréntesis vacíos
setTimeout(() => console.log('Han pasado dos segundos'), 2000);

La sintaxis tiene algunas abreviaturas más, que conviene reconocer al leer código:

Caso Forma
Un único parámetro n => n * 2 (los paréntesis son opcionales)
Ningún parámetro () => console.log('hola')
Cuerpo de una expresión (a, b) => a + b (el return es implícito)
Cuerpo con varias líneas (a, b) => { const s = a + b; return s; }
Devolver un objeto n => ({ valor: n }) (el objeto va entre paréntesis)

Hay dos diferencias reales entre la declaración y las otras dos formas. La primera es el hoisting: una declaración de función se puede llamar antes de la línea en la que aparece, porque el motor la registra al entrar en el ámbito. La segunda afecta a las flechas, que no tienen su propio this; tiene mucha importancia al trabajar con objetos y la verás en la unidad de clases.

Si escribes una flecha con llaves, el return deja de ser implícito. n => { n * 2 } devuelve undefined, porque las llaves abren un bloque de código, no un valor. Es uno de los errores más repetidos con map y filter.

2 // Parámetros sin contrato

JavaScript no verifica cuántos argumentos recibe una función ni de qué tipo son. Si faltan, valen undefined; si sobran, se ignoran. Por eso existen dos herramientas para dejar claro qué espera una función:

// Parámetro por defecto: se usa si el argumento falta o es undefined
function saludar(nombre = 'invitado') {
  return `Hola, ${nombre}`;
}
saludar();          // 'Hola, invitado'
saludar('Marta');   // 'Hola, Marta'

// Parámetro rest: agrupa en un array todos los argumentos restantes
function media(...notas) {
  let suma = 0;
  for (const nota of notas) {
    suma += nota;
  }
  return suma / notas.length;
}
media(7, 8, 9);     // 8

Esto tiene una consecuencia que choca mucho viniendo de Java: como el número de parámetros no forma parte de la función, no hay forma de tener dos versiones de una misma función que se distingan por lo que reciben.

function area(lado) {
  return lado * lado;
}

function area(base, altura) {   // no es otra versión: sustituye a la de arriba
  return base * altura;
}

area(4);      // NaN: se ejecuta la segunda, con altura = undefined
area(4, 3);   // 12

En JavaScript no hay sobrecarga de métodos como en Java. Si defines dos funciones con el mismo nombre, la segunda sustituye a la primera, sin error ni aviso. Y por el hoisting, incluso las llamadas escritas antes de la segunda declaración usan ya la segunda.

Cuando una función debe comportarse distinto según lo que reciba, se escribe una sola función que lo decide dentro, con parámetros por defecto o comprobando los valores:

function area(base, altura = base) {   // si falta altura, es un cuadrado
  return base * altura;
}

area(4);      // 16
area(4, 3);   // 12

3 // Pasar funciones como argumento

Como una función es un valor, puedes pasarla a otra función. La función que se pasa se llama callback, y la que la recibe, función de orden superior. Es la manera de decir «haz esto, y cuando llegue el momento, llama a esto otro»:

UNA FUNCIÓN QUE RECIBE OTRA FUNCIÓN const doble = n => n * 2 const lista = [1, 2, 3] aplicar(lista, fn) recorre la lista y llama a fn fn(1) → 2 fn(2) → 4 fn(3) → 6 [2, 4, 6] se pasa doble, sin paréntesis: la función, no su resultado aplicar no sabe qué hace fn: sólo sabe cuándo llamarla
Sigue la función doble: sale de una constante, entra en aplicar como argumento y es aplicar quien decide llamarla, una vez por cada elemento.
function aplicar(lista, fn) {
  const resultado = [];
  for (const elemento of lista) {
    resultado.push(fn(elemento));
  }
  return resultado;
}

const doble = n => n * 2;
aplicar([1, 2, 3], doble);              // [2, 4, 6]
aplicar([1, 2, 3], n => n + 10);        // [11, 12, 13]

aplicar no sabe qué transformación se va a hacer; sólo sabe recorrer una lista. Quien la llama decide el comportamiento pasando otra función. Este patrón está en todas partes: addEventListener recibe la función que se ejecutará al hacer clic, setTimeout la que se ejecutará pasado un tiempo y map la que transforma cada elemento.

Escribir el nombre de una función sin paréntesis es hablar de la función; con paréntesis, es ejecutarla. Al pasar un callback casi siempre quieres lo primero.

4 // Funciones que devuelven funciones

El paso contrario también es posible: una función puede fabricar y devolver otra función.

function crearMultiplicador(factor) {
  return numero => numero * factor;
}

const triple = crearMultiplicador(3);
const porCien = crearMultiplicador(100);

triple(5);    // 15
porCien(0.21); // 21

Fíjate en algo que no es evidente: cuando llamas a triple(5), crearMultiplicador ya terminó hace rato, y aun así la función devuelta sigue sabiendo que factor vale 3. Eso es un closure, y es el tema de la unidad siguiente.

Quieres formatear precios de distintas formas: con euros, con dos decimales y con IVA incluido, sin repetir la lógica de recorrer la lista.

  1. Separas el qué del cómo: una función formatear(precios, fn) recorre la lista y aplica fn a cada precio.
  2. Defines cada formato como una función pequeña: p => p.toFixed(2) + ' €' y p => (p * 1.21).toFixed(2) + ' €'.
  3. Llamas a la misma función con distintos callbacks: formatear([10, 4.5], p => p.toFixed(2) + ' €') da ['10.00 €', '4.50 €'].
  4. Fabricas variantes con una función que devuelve funciones: const conImpuesto = tipo => p => (p * (1 + tipo)).toFixed(2) + ' €'; y después formatear(precios, conImpuesto(0.21)).
El recorrido se escribe una sola vez y cada formato nuevo es una línea. Esa es la ventaja de tratar las funciones como valores: el comportamiento se puede pasar de un sitio a otro igual que un dato.

5 // Para practicar

Los primeros ejercicios practican la sintaxis de funciones; los dos últimos, pasar funciones como argumento. Cada ejercicio trae el enunciado en los comentarios del código.

Valor absolutotraining-daw-sv/modulo-00/03-funciones-como-valores/01_absPares anterior y siguientetraining-daw-sv/modulo-00/03-funciones-como-valores/02_next_pairsNúmeros parestraining-daw-sv/modulo-00/03-funciones-como-valores/03_even_numbersPing pongtraining-daw-sv/modulo-00/03-funciones-como-valores/04_ping_pongTablas de multiplicartraining-daw-sv/modulo-00/03-funciones-como-valores/05_multiplicationsParámetros por defecto y resttraining-daw-sv/modulo-00/03-funciones-como-valores/06_advanced_parametersUn filtro que recibe una condicióntraining-daw-sv/modulo-00/03-funciones-como-valores/07_custom_filterEl mismo filtro con funciones flechatraining-daw-sv/modulo-00/03-funciones-como-valores/08_arrow_custom_filter

Autoevaluación

¿Qué diferencia hay entre boton.addEventListener('click', saludar) y boton.addEventListener('click', saludar())?
La primera pasa la función; la segunda la ejecuta y pasa lo que devuelve. Sin paréntesis, saludar es el valor función, que el navegador llamará al hacer clic. Con paréntesis, saludar() se ejecuta en ese mismo instante y lo que se registra es su resultado, normalmente undefined.
Puedes llamar a una función declarada con function sumar() {} antes de la línea en la que aparece, pero no a const sumar = () => {}. ¿Por qué?
Las declaraciones de función se procesan antes de ejecutar el código (hoisting). El motor registra sumar al entrar en el ámbito, así que existe desde la primera línea. Con const, la variable existe pero no tiene valor hasta que se ejecuta su línea, y usarla antes lanza un error.
Llamas a sumar(2) con una función function sumar(a, b) { return a + b; }. En Java no compilaría; en JavaScript devuelve NaN. Explica el recorrido.
JavaScript no comprueba el número de argumentos. El parámetro que falta, b, vale undefined, y 2 + undefined convierte undefined a número, que da NaN. Un parámetro por defecto, b = 0, evitaría el problema.