Módulo 05: Funciones

Objetivos del módulo

  • Entender qué es una función y por qué es la pieza básica de cualquier programa
  • Crear funciones con declaraciones, expresiones y funciones flecha (arrow functions)
  • Dominar los parámetros: argumentos, valores por defecto, parámetros rest y el antiguo objeto arguments
  • Devolver valores con return y aplicar el retorno temprano
  • Tratar las funciones como valores: callbacks y funciones de orden superior
  • Comprender el ámbito, la cadena de ámbitos y los closures, con ejemplos prácticos
  • Conocer el hoisting de funciones, las IIFE y la recursión
  • Escribir funciones puras y controlar los efectos secundarios
  • Entender la diferencia básica de this entre funciones normales y flecha
  • Usar el patrón de opciones con desestructuración y documentar funciones con JSDoc

1. ¿Qué es una función?

📘 Concepto: Una función es un bloque de código con nombre que realiza una tarea concreta y que puedes ejecutar (llamar) tantas veces como quieras. Piensa en una máquina de zumos: le metes ingredientes (los parámetros), hace su trabajo por dentro y te devuelve un resultado (el valor de retorno). No necesitas saber cómo funciona por dentro para usarla.

Sin funciones, tendrías que repetir el mismo código una y otra vez:

// ❌ Sin funciones: código repetido
const precio1 = 50;
console.log(`Total: ${(precio1 * 1.21).toFixed(2)} €`);
const precio2 = 19.99;
console.log(`Total: ${(precio2 * 1.21).toFixed(2)} €`);

// ✅ Con una función: se escribe una vez y se usa muchas
function mostrarConIVA(precio) {
  console.log(`Total: ${(precio * 1.21).toFixed(2)} €`);
}

mostrarConIVA(50);    // Total: 60.50 €
mostrarConIVA(19.99); // Total: 24.19 €
Ventaja Qué significa
Reutilización Escribes el código una vez y lo usas en muchos sitios
Mantenimiento Si el IVA cambia, lo corriges en un solo lugar
Legibilidad calcularTotal(carrito) se entiende sin leer su interior
Pruebas Una función pequeña es fácil de probar por separado (módulo 21)
Abstracción Divides un problema grande en piezas pequeñas con nombre

2. Declaración de función

function sumar(a, b) {
  return a + b;
}

const resultado = sumar(2, 3); // llamada a la función
console.log(resultado);        // 5
console.log(sumar(10, 20));    // 30
Parte Significado
function Palabra clave que crea la función
sumar Nombre de la función (camelCase, normalmente un verbo)
(a, b) Parámetros: variables que recibirán los datos
{ ... } Cuerpo: el código que se ejecuta
return a + b; Devuelve el resultado a quien llamó
sumar(2, 3) Llamada: ejecuta la función con los argumentos 2 y 3

⚠️ Importante: Escribir sumar (sin paréntesis) no ejecuta la función: se refiere a la función en sí. Para ejecutarla necesitas los paréntesis: sumar(2, 3). Esta diferencia será clave en la sección 7.

💡 Consejo: Nombra las funciones con un verbo que describa lo que hacen: calcularTotal, validarEmail, mostrarMensaje, obtenerUsuario. Las que devuelven un booleano suelen empezar por es, tiene o puede: esPar, tieneStock.


3. Expresión de función

Una función también se puede crear como un valor y guardarse en una variable:

const restar = function (a, b) {
  return a - b;
};

console.log(restar(10, 4)); // 6

La función de arriba es anónima (no tiene nombre propio; JavaScript le asigna el de la variable). También puedes darle un nombre, útil para recursión y para que aparezca en los mensajes de error:

const factorial = function calcularFactorial(n) {
  return n <= 1 ? 1 : n * calcularFactorial(n - 1);
};
console.log(factorial(5)); // 120

4. Funciones flecha (arrow functions)

ES2015 añadió una sintaxis más corta, la función flecha, muy usada en JavaScript moderno:

const multiplicar = (a, b) => {
  return a * b;
};

4.1 Retorno implícito

Si el cuerpo es una sola expresión, puedes quitar las llaves y el return: el resultado se devuelve automáticamente:

const multiplicar = (a, b) => a * b;
const cuadrado = (n) => n * n;   // con un solo parámetro, los paréntesis son opcionales: n => n * n
const saludar = () => 'Hola';    // sin parámetros, paréntesis vacíos obligatorios

console.log(multiplicar(3, 4)); // 12
console.log(cuadrado(5));       // 25
console.log(saludar());         // Hola

💡 Consejo: Prettier, por defecto, pone siempre paréntesis alrededor de los parámetros ((n) => n * n). Es la convención que seguimos en el curso: así añadir un segundo parámetro no obliga a cambiar nada más.

4.2 Devolver un objeto: paréntesis

Si quieres devolver un objeto literal con retorno implícito, envuélvelo entre paréntesis. Si no, JavaScript interpreta las llaves como el cuerpo de la función:

// ❌ Las llaves se interpretan como un bloque: devuelve undefined
const crearUsuarioMal = (nombre, edad) => { nombre, edad };
console.log(crearUsuarioMal('Ana', 28)); // undefined

// ✅ Con paréntesis, es un objeto
const crearUsuario = (nombre, edad) => ({ nombre, edad });
console.log(crearUsuario('Ana', 28)); // { nombre: 'Ana', edad: 28 }

📘 Concepto: { nombre, edad } es la abreviatura de { nombre: nombre, edad: edad } (shorthand properties), que verás en el módulo 07.

4.3 Comparativa de las tres formas

Característica Declaración Expresión Flecha
Sintaxis function f() {} const f = function () {}; const f = () => {};
Hoisting (usable antes de definirla) Sí No No
Retorno implícito No No Sí (una expresión)
this propio Sí Sí No (lo hereda)
Objeto arguments Sí Sí No
Se puede usar con new Sí Sí No
Uso típico Funciones principales de un módulo Asignar funciones condicionalmente Callbacks, funciones cortas

💡 Consejo: Una guía práctica: usa declaraciones para las funciones “con nombre” importantes de tu programa y funciones flecha para callbacks y funciones cortas. Lo verás en todos los módulos siguientes.


5. Parámetros y argumentos

📘 Concepto: Los parámetros son los nombres que aparecen en la definición de la función. Los argumentos son los valores concretos que pasas al llamarla.

//               parámetros
function saludar(nombre, saludo) {
  return `${saludo}, ${nombre}`;
}

//                 argumentos
console.log(saludar('Ana', 'Buenos días')); // Buenos días, Ana

5.1 Menos o más argumentos de la cuenta

JavaScript no comprueba el número de argumentos:

function sumar(a, b) {
  return a + b;
}

console.log(sumar(2, 3));    // 5
console.log(sumar(2));       // NaN  (b es undefined, y 2 + undefined = NaN)
console.log(sumar(2, 3, 4)); // 5    (el 4 se ignora)

5.2 Parámetros por defecto

Desde ES2015 puedes dar un valor por defecto a un parámetro. Se usa cuando el argumento no se pasa o es undefined:

function saludar(nombre = 'invitado', saludo = `Hola, ${nombre}`) {
  return saludo;
}

console.log(saludar());          // Hola, invitado
console.log(saludar('Ana'));     // Hola, Ana
console.log(saludar(undefined)); // Hola, invitado
console.log(saludar(null));      // Hola, null ← null NO activa el valor por defecto

📘 Concepto: Como ves en saludo, un valor por defecto puede usar parámetros anteriores. Se evalúa en cada llamada, no una sola vez.

5.3 Parámetros rest ...

Un parámetro precedido de ... recoge todos los argumentos restantes en un array:

function sumarTodo(...numeros) {
  let total = 0;
  for (const n of numeros) {
    total += n;
  }
  return total;
}

console.log(sumarTodo());        // 0
console.log(sumarTodo(1, 2, 3)); // 6

// Se puede combinar con parámetros normales (el rest siempre va al final)
function registrar(nivel, ...mensajes) {
  return `[${nivel}] ${mensajes.join(' ')}`;
}
console.log(registrar('INFO', 'Servidor', 'iniciado', 'en', 'puerto', 3000));
// [INFO] Servidor iniciado en puerto 3000

💡 Consejo: No confundas rest y spread, que usan los mismos tres puntos. En la definición (function f(...args)) los puntos recogen argumentos en un array (rest). En la llamada (f(...array)) los esparcen (spread): sumarTodo(...[4, 5, 6]) da 15.

5.4 El objeto arguments (legado)

Antes de los parámetros rest, las funciones normales tenían un objeto especial arguments con todos los argumentos recibidos:

function antigua() {
  console.log(arguments.length);         // 3
  console.log(Array.isArray(arguments)); // false: parece un array, pero no lo es
  console.log(Array.from(arguments));    // [ 1, 'dos', true ]
}
antigua(1, 'dos', true);

⚠️ Importante: arguments no existe en las funciones flecha y no es un array de verdad. En código moderno usa siempre parámetros rest (...args). Solo necesitas reconocer arguments cuando leas código antiguo.

5.5 ¿Qué pasa si modifico un parámetro?

Los primitivos se pasan por valor (la función recibe una copia); los objetos se pasan compartiendo la referencia (módulo 02):

function duplicar(numero) {
  numero = numero * 2; // modifica la copia local
}
let x = 10;
duplicar(x);
console.log(x); // 10 (no cambia)

function cumplirAnios(persona) {
  persona.edad++; // modifica el MISMO objeto que tiene quien llama
}
const ana = { nombre: 'Ana', edad: 28 };
cumplirAnios(ana);
console.log(ana.edad); // 29 (¡ha cambiado!)

6. Devolver valores: return

return hace dos cosas: termina la función y devuelve un valor a quien la llamó.

function esMayorDeEdad(edad) {
  return edad >= 18;
}

if (esMayorDeEdad(20)) {
  console.log('Acceso permitido');
}

Una función sin return (o con return; sin valor) devuelve undefined:

function sinReturn() {}
console.log(sinReturn()); // undefined

6.1 Retorno temprano

Como return termina la función, puedes usarlo para salir en cuanto sepas la respuesta (las guard clauses del módulo 04):

function calcularDescuento(total, esSocio) {
  if (total <= 0) {
    return 0; // caso inválido: salimos pronto
  }
  if (esSocio) {
    return total * 0.15;
  }
  return total > 100 ? total * 0.05 : 0;
}

console.log(calcularDescuento(200, true));  // 30
console.log(calcularDescuento(200, false)); // 10
console.log(calcularDescuento(50, false));  // 0

6.2 Devolver varios valores

Una función devuelve un solo valor, pero ese valor puede ser un array o un objeto:

function estadisticas(numeros) {
  let suma = 0;
  for (const n of numeros) {
    suma += n;
  }
  return {
    suma,
    media: suma / numeros.length,
    cantidad: numeros.length,
  };
}

const { suma, media } = estadisticas([4, 8, 6]); // desestructuración (módulo 07)
console.log(suma, media); // 18 6

6.3 Trampa: return y el salto de línea

function obtenerConfig() {
  return      // ← JavaScript inserta aquí un punto y coma automático
  {
    tema: 'oscuro'
  };
}
console.log(obtenerConfig()); // undefined ❌ (las llaves se han leído como un bloque suelto)

⚠️ Importante: Nunca pongas un salto de línea justo después de return. Abre la llave (o empieza la expresión) en la misma línea: return {.


7. Las funciones son valores (first-class functions)

📘 Concepto: En JavaScript las funciones son ciudadanas de primera clase: son valores como un número o un texto. Puedes guardarlas en variables, meterlas en arrays y objetos, pasarlas como argumento a otras funciones y devolverlas como resultado.

const saludar = (nombre) => `Hola, ${nombre}`;

// Guardar en otra variable
const decirHola = saludar;
console.log(decirHola('Ana')); // Hola, Ana

// Guardar en un objeto o un array
const acciones = {
  saludar,
  despedir: (nombre) => `Adiós, ${nombre}`,
};
console.log(acciones.despedir('Luis')); // Adiós, Luis

const transformaciones = [(t) => t.trim(), (t) => t.toUpperCase()];
let texto = '  hola  ';
for (const transformar of transformaciones) {
  texto = transformar(texto);
}
console.log(texto); // HOLA

7.1 Callbacks

Un callback es una función que pasas como argumento a otra para que ésta la llame cuando le convenga:

function repetir(veces, accion) {
  for (let i = 0; i < veces; i++) {
    accion(i); // llamamos al callback
  }
}

repetir(3, (i) => console.log(`Vuelta ${i}`));
// Vuelta 0
// Vuelta 1
// Vuelta 2

Ya has usado callbacks sin saberlo:

setTimeout(() => console.log('Ha pasado 1 segundo'), 1000); // el callback se llama después
// boton.addEventListener('click', () => { ... });           // se llama al hacer clic

⚠️ Importante: Pasa la función, no su resultado. setTimeout(saludar, 1000) pasa la función; setTimeout(saludar(), 1000) la ejecuta ya y pasa lo que devuelve (probablemente undefined).

7.2 Funciones de orden superior

Una función de orden superior (higher-order function) es la que recibe funciones como argumento o devuelve una función. repetir es una. Los arrays tienen muchas (las verás a fondo en el módulo 06):

const numeros = [1, 2, 3, 4];
console.log(numeros.map((n) => n * 2));         // [ 2, 4, 6, 8 ]
console.log(numeros.filter((n) => n % 2 === 0)); // [ 2, 4 ]

Y puedes crear las tuyas, por ejemplo una que “envuelve” otra función para añadirle registro:

function conRegistro(fn) {
  return (...args) => {
    console.log(`Llamando a ${fn.name} con`, args);
    const resultado = fn(...args);
    console.log(`Resultado: ${resultado}`);
    return resultado;
  };
}

function sumar(a, b) {
  return a + b;
}

const sumarConRegistro = conRegistro(sumar);
sumarConRegistro(2, 3);
// Llamando a sumar con [ 2, 3 ]
// Resultado: 5

8. Ámbito (scope)

📘 Concepto: El ámbito de una variable es la zona del código donde esa variable existe y es accesible. Piensa en las habitaciones de una casa con ventanas de espejo: desde dentro de una habitación ves el pasillo, pero desde el pasillo no ves lo que hay dentro de las habitaciones.

Ámbito Se crea con Visible en
Global (o de módulo) Variables fuera de cualquier función o bloque Todo el fichero
De función Variables y parámetros dentro de una función Solo dentro de esa función
De bloque let/const dentro de { } (if, for…) Solo dentro de ese bloque
const app = 'Tienda';          // ámbito de módulo

function procesar() {
  const pedido = 1042;         // ámbito de función

  if (pedido > 1000) {
    const urgente = true;      // ámbito de bloque
    console.log(app, pedido, urgente); // Tienda 1042 true ✅ ve las tres
  }

  // console.log(urgente);     ❌ ReferenceError: fuera de su bloque
}

procesar();
// console.log(pedido);        ❌ ReferenceError: fuera de su función

📘 Concepto: En un módulo ES, las variables “globales” en realidad son del módulo: no son visibles desde otros ficheros salvo que las exportes (módulo 13). Es una gran ventaja frente a los scripts clásicos.

8.1 La cadena de ámbitos

Cuando usas una variable, JavaScript la busca primero en el ámbito actual; si no la encuentra, sube al ámbito que lo contiene, y así sucesivamente hasta el global. Esta búsqueda hacia fuera es la cadena de ámbitos (scope chain):

const x = 'global';

function externa() {
  const y = 'externa';

  function interna() {
    const z = 'interna';
    return `${x} ${y} ${z}`; // z: aquí; y: un nivel arriba; x: dos niveles arriba
  }

  return interna();
}

console.log(externa()); // global externa interna
flowchart TB
    subgraph G["Ámbito global: x"]
        subgraph E["Ámbito de externa: y"]
            subgraph I["Ámbito de interna: z"]
                U["busca z → y → x"]
            end
        end
    end

⚠️ Importante: La búsqueda va solo hacia fuera, nunca hacia dentro. Y si declaras una variable con el mismo nombre en un ámbito interior, oculta (shadowing) a la exterior. Es legal, pero confuso: evita repetir nombres.


9. Closures

📘 Concepto: Un closure (clausura) es una función que recuerda las variables del ámbito donde se creó, incluso cuando se ejecuta fuera de él y aunque la función exterior ya haya terminado. Es como una mochila: la función interna se lleva consigo las variables que necesita.

9.1 Contador privado

function crearContador() {
  let cuenta = 0; // variable "privada": nadie de fuera puede tocarla directamente

  return {
    incrementar: () => ++cuenta,
    decrementar: () => --cuenta,
    valor: () => cuenta,
  };
}

const contadorA = crearContador();
const contadorB = crearContador(); // cada llamada crea su propia "mochila"

contadorA.incrementar();
contadorA.incrementar();
contadorB.incrementar();

console.log(contadorA.valor()); // 2
console.log(contadorB.valor()); // 1
console.log(contadorA.cuenta);  // undefined: la variable no es accesible desde fuera

9.2 Fábrica de funciones

Una función que fabrica otras funciones configuradas:

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

const doble = crearMultiplicador(2);
const triple = crearMultiplicador(3);

console.log(doble(5));  // 10
console.log(triple(5)); // 15

// Un ejemplo más realista: formateadores de precio
function crearFormateador(moneda) {
  return (cantidad) => `${cantidad.toFixed(2)} ${moneda}`;
}
const enEuros = crearFormateador('€');
console.log(enEuros(19.9)); // 19.90 €

9.3 Memoización

Memoizar es guardar los resultados de una función costosa para no recalcularlos cuando se repiten los mismos argumentos. El closure guarda la “caché”:

function memoizar(fn) {
  const cache = new Map(); // Map es un diccionario clave → valor (módulo 11)

  return (n) => {
    if (cache.has(n)) {
      return cache.get(n); // ya lo calculamos antes
    }
    const resultado = fn(n);
    cache.set(n, resultado);
    return resultado;
  };
}

let llamadas = 0;
function calculoLento(n) {
  llamadas++;
  let suma = 0;
  for (let i = 0; i < 1_000_000; i++) {
    suma += i % n;
  }
  return suma;
}

const calculoRapido = memoizar(calculoLento);
console.log(calculoRapido(7)); // 2999997 (calculado)
console.log(calculoRapido(7)); // 2999997 (desde la caché, instantáneo)
console.log(calculoRapido(8)); // 3500000 (calculado)
console.log(llamadas);         // 2: solo se ejecutó dos veces de verdad

⚠️ Importante: Solo tiene sentido memoizar funciones puras (sección 13): si el resultado dependiera de algo externo que cambia, la caché devolvería datos obsoletos.


10. Hoisting de funciones

Las declaraciones de función se elevan completas al principio de su ámbito: puedes llamarlas antes de escribirlas. Las expresiones y las flechas siguen las reglas de const/let (TDZ, módulo 02):

console.log(declarada()); // funciona antes de declararse ✅

function declarada() {
  return 'funciona antes de declararse';
}

expresion(); // ❌ ReferenceError: Cannot access 'expresion' before initialization
const expresion = function () {};

💡 Consejo: Que se pueda no significa que haya que hacerlo siempre. Un estilo habitual y legible es escribir el código principal arriba y las funciones auxiliares (declaraciones) abajo, aprovechando el hoisting; o bien definir todo antes de usarlo. Elige uno y sé coherente.


11. IIFE (histórico)

Una IIFE (Immediately Invoked Function Expression) es una función que se define y se ejecuta en el mismo momento:

const resultado = (function () {
  const privado = 42; // no "contamina" el ámbito exterior
  return privado * 2;
})();

console.log(resultado); // 84

// Versión con flecha
(() => {
  console.log('Me ejecuto una sola vez');
})();

📘 Concepto: Antes de los módulos ES y de let/const, la IIFE era la única forma de crear un ámbito privado y no llenar window de variables globales. Hoy, como cada módulo tiene su propio ámbito y existen los bloques, apenas se necesita. La verás en código antiguo y en algún caso puntual, como ejecutar código asíncrono en entornos sin await de nivel superior.


12. Recursión

📘 Concepto: Una función recursiva es la que se llama a sí misma. Divide un problema en una versión más pequeña del mismo problema, hasta llegar a un caso base que se resuelve directamente. Es como las muñecas rusas: abres una y dentro hay otra igual pero más pequeña, hasta llegar a la última, que ya no se abre.

Toda función recursiva necesita:

  1. Un caso base: la condición que detiene la recursión.
  2. Un caso recursivo: la llamada a sí misma con un problema más pequeño.

12.1 Factorial

El factorial de n (n!) es n × (n-1) × ... × 1. Se define recursivamente como n! = n × (n-1)!, con 1! = 1:

function factorial(n) {
  if (n <= 1) {
    return 1; // caso base
  }
  return n * factorial(n - 1); // caso recursivo
}

console.log(factorial(5));  // 120
console.log(factorial(10)); // 3628800
factorial(4)
= 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * 1))          ← caso base
= 24

12.2 Fibonacci y el coste de la recursión

En la sucesión de Fibonacci cada número es la suma de los dos anteriores: 0, 1, 1, 2, 3, 5, 8, 13…

let llamadas = 0;

function fibonacci(n) {
  llamadas++;
  if (n < 2) {
    return n; // casos base: fib(0) = 0, fib(1) = 1
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(30)); // 832040
console.log(llamadas);      // 2692537 ← ¡más de 2,6 millones de llamadas!

La versión directa repite los mismos cálculos una y otra vez (fibonacci(28) se calcula muchas veces). Con memoización el coste se desploma:

const memo = new Map();

function fibonacciMemo(n) {
  if (n < 2) {
    return n;
  }
  if (memo.has(n)) {
    return memo.get(n);
  }
  const resultado = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
  memo.set(n, resultado);
  return resultado;
}

console.log(fibonacciMemo(50)); // 12586269025 (al instante)

12.3 Recorrer un árbol

La recursión brilla con estructuras anidadas de profundidad desconocida, como carpetas dentro de carpetas:

const proyecto = {
  nombre: 'src',
  hijos: [
    { nombre: 'index.js', tamanio: 120 },
    {
      nombre: 'utils',
      hijos: [
        { nombre: 'fecha.js', tamanio: 80 },
        { nombre: 'texto.js', tamanio: 45 },
      ],
    },
    { nombre: 'estilos.css', tamanio: 300 },
  ],
};

function mostrarArbol(nodo, nivel = 0) {
  const sangria = '  '.repeat(nivel);
  const icono = nodo.hijos ? '📁' : '📄';
  console.log(`${sangria}${icono} ${nodo.nombre}`);

  for (const hijo of nodo.hijos ?? []) {
    mostrarArbol(hijo, nivel + 1); // la misma función, un nivel más abajo
  }
}

function tamanioTotal(nodo) {
  if (!nodo.hijos) {
    return nodo.tamanio; // caso base: un fichero
  }
  let total = 0;
  for (const hijo of nodo.hijos) {
    total += tamanioTotal(hijo);
  }
  return total;
}

mostrarArbol(proyecto);
// 📁 src
//   📄 index.js
//   📁 utils
//     📄 fecha.js
//     📄 texto.js
//   📄 estilos.css

console.log(tamanioTotal(proyecto)); // 545

⚠️ Importante: Cada llamada ocupa espacio en la pila de llamadas. Si olvidas el caso base o la recursión es demasiado profunda (decenas de miles de niveles), obtendrás RangeError: Maximum call stack size exceeded. Para repeticiones muy largas y simples, un bucle es más seguro.


13. Funciones puras y efectos secundarios

📘 Concepto: Una función pura cumple dos reglas: (1) con los mismos argumentos siempre devuelve el mismo resultado, y (2) no produce efectos secundarios: no modifica nada fuera de ella (variables externas, sus argumentos, la página, ficheros…).

// ✅ Pura: solo depende de sus parámetros y no toca nada externo
function calcularIVA(precio, tipo = 0.21) {
  return precio * tipo;
}

// ❌ Impura: depende de y modifica una variable externa
let total = 0;
function sumarAlTotal(cantidad) {
  total += cantidad; // efecto secundario
  return total;
}
console.log(sumarAlTotal(5)); // 5
console.log(sumarAlTotal(5)); // 10 ← mismos argumentos, distinto resultado

// ❌ Impura: modifica el array que recibe
function anadirProductoMal(carrito, producto) {
  carrito.push(producto);
  return carrito;
}

// ✅ Pura: devuelve un array NUEVO sin tocar el original (spread, módulo 06)
function anadirProducto(carrito, producto) {
  return [...carrito, producto];
}
Efectos secundarios habituales Ejemplo
Modificar variables externas total += x
Modificar los argumentos persona.edad++, lista.push(x)
Escribir en la página elemento.textContent = ...
Mostrar mensajes console.log, alert
Red, ficheros, almacenamiento fetch(...), localStorage.setItem(...)
Depender de algo que cambia Math.random(), new Date()

💡 Consejo: Los efectos secundarios no son malos: un programa sin ellos no haría nada visible. La idea profesional es separarlos: mantén la lógica (cálculos, validaciones, transformaciones) en funciones puras, fáciles de probar y reutilizar, y concentra los efectos (pintar en la página, guardar, pedir datos) en pocos sitios bien identificados.


14. this en funciones normales y flecha (introducción)

📘 Concepto: this es una palabra especial que, dentro de una función normal, se refiere al objeto que la llamó (el que está antes del punto). Su valor se decide al llamar a la función, no al escribirla. Las funciones flecha no tienen this propio: usan el this del lugar donde se escribieron.

const persona = {
  nombre: 'Ana',

  saludarNormal() {
    return `Hola, soy ${this.nombre}`; // this = el objeto antes del punto
  },

  saludarFlecha: () => `Hola, soy ${this?.nombre}`, // this NO es persona

  saludarConRetraso() {
    // La flecha hereda el this de saludarConRetraso, que sí es persona
    setTimeout(() => console.log(`(un momento después) Soy ${this.nombre}`), 0);
  },
};

console.log(persona.saludarNormal()); // Hola, soy Ana
console.log(persona.saludarFlecha()); // Hola, soy undefined
persona.saludarConRetraso();          // (un momento después) Soy Ana

// Si separas el método de su objeto, pierde su this
const suelta = persona.saludarNormal;
suelta(); // ❌ TypeError: Cannot read properties of undefined (reading 'nombre')
Tipo de función Valor de this
Función normal llamada como objeto.metodo() objeto
Función normal llamada “suelta” f() (modo estricto) undefined
Función flecha El this del ámbito que la rodea

💡 Consejo: Regla práctica de momento: usa la sintaxis de método (saludar() { ... }) para las funciones dentro de objetos que necesitan this, y funciones flecha para los callbacks dentro de esos métodos. Estudiarás this, call, apply y bind a fondo en el módulo 07.


15. El patrón de opciones con desestructuración

Cuando una función tiene muchos parámetros, las llamadas se vuelven ilegibles y el orden es fácil de confundir:

// ❌ ¿Qué significa cada valor?
crearBotonMal('Guardar', 'azul', 'grande', false, true);

La solución profesional es recibir un único objeto de opciones y desestructurarlo en la lista de parámetros, con valores por defecto:

function crearBoton({ texto, color = 'azul', tamanio = 'mediano', deshabilitado = false } = {}) {
  return `${texto} | ${color} | ${tamanio} | ${deshabilitado}`;
}

console.log(crearBoton({ texto: 'Guardar' }));
// Guardar | azul | mediano | false

console.log(crearBoton({ texto: 'Borrar', color: 'rojo', deshabilitado: true }));
// Borrar | rojo | mediano | true   ← el orden de las opciones da igual

console.log(crearBoton());
// undefined | azul | mediano | false ← gracias al "= {}" final no da error
Ventaja Explicación
Llamadas autoexplicativas Cada valor lleva su nombre: { color: 'rojo' }
Orden libre No hay que recordar posiciones
Opciones omitibles Solo pasas lo que quieres cambiar
Fácil de ampliar Añadir una opción nueva no rompe las llamadas existentes

⚠️ Importante: El = {} final es el valor por defecto del objeto entero. Sin él, llamar a crearBoton() sin argumentos lanza TypeError: Cannot destructure property 'texto' of 'undefined' as it is undefined.


16. Documentar funciones con JSDoc

JSDoc es un formato estándar de comentarios para documentar funciones. Empieza con /** y usa etiquetas que empiezan por @. VS Code lo lee y te muestra la documentación y los tipos al pasar el ratón o al autocompletar:

/**
 * Calcula el precio final de un producto aplicando IVA y descuento.
 *
 * @param {number} precio - Precio base sin IVA, en euros.
 * @param {object} [opciones] - Opciones de cálculo.
 * @param {number} [opciones.iva=0.21] - Tipo de IVA (0.21 = 21 %).
 * @param {number} [opciones.descuento=0] - Descuento entre 0 y 1.
 * @returns {number} El precio final redondeado a 2 decimales.
 * @throws {RangeError} Si el precio es negativo.
 * @example
 * calcularPrecioFinal(100, { descuento: 0.1 }); // 108.9
 */
function calcularPrecioFinal(precio, { iva = 0.21, descuento = 0 } = {}) {
  if (precio < 0) {
    throw new RangeError('El precio no puede ser negativo');
  }
  const final = precio * (1 - descuento) * (1 + iva);
  return Math.round(final * 100) / 100;
}

console.log(calcularPrecioFinal(100, { descuento: 0.1 })); // 108.9
Etiqueta Para qué
@param {tipo} nombre - descripción Describe un parámetro
@param {tipo} [nombre=valor] Parámetro opcional con su valor por defecto
@returns {tipo} descripción Qué devuelve
@throws {TipoError} descripción Qué errores puede lanzar (módulo 10)
@example Un ejemplo de uso

💡 Consejo: Documenta sobre todo el qué y el por qué, no el cómo (eso ya lo dice el código). En el módulo 21 verás que VS Code puede incluso comprobar los tipos de JSDoc añadiendo // @ts-check al principio del fichero.


17. Ejercicios

Ejercicio 1: Conversor de temperaturas

Escribe tres funciones flecha con retorno implícito: celsiusAFahrenheit (°F = °C × 9/5 + 32), fahrenheitACelsius y celsiusAKelvin (K = °C + 273.15). Documenta cada una con JSDoc y muestra una tabla de conversión de -10 °C a 40 °C de 10 en 10 con console.table.

Ejercicio 2: Validador de contraseñas

Crea una función validarContrasena(texto) que devuelva un objeto { valida, errores }, donde errores es un array de mensajes. Debe comprobar: al menos 8 caracteres, al menos un número, al menos una mayúscula y que no contenga espacios. Usa guard clauses para el caso de que no reciba un texto (typeof texto !== 'string'). Pista: recorre el texto con for...of y compara cada carácter (c >= '0' && c <= '9', c !== c.toLowerCase()).

Ejercicio 3: Funciones de orden superior propias

Sin usar los métodos de array, escribe tus propias versiones miMap(array, fn), miFilter(array, fn) y miReduce(array, fn, inicial) usando bucles for...of y callbacks. Pruébalas para duplicar números, quedarte con los pares y sumar todos los elementos.

Ejercicio 4: Generador de IDs con closure

Crea una función crearGeneradorIds(prefijo) que devuelva una función; cada vez que se llame, debe devolver un ID nuevo con el prefijo y un número correlativo con 4 cifras: 'USR-0001', 'USR-0002'… Dos generadores con prefijos distintos deben llevar cuentas independientes. Pista: String(n).padStart(4, '0').

Ejercicio 5: Recursión

Implementa de forma recursiva: (a) potencia(base, exponente) sin usar **; (b) sumarDigitos(n), que suma los dígitos de un número (sumarDigitos(1234) → 10); (c) invertir(texto), que invierte una cadena ('hola' → 'aloh'); y (d) contarFicheros(nodo), que cuenta los ficheros del árbol de la sección 12.3.

Ejercicio 6: Memoización genérica

Generaliza la función memoizar para que funcione con funciones de varios argumentos (pista: usa ...args y construye la clave de la caché con JSON.stringify(args)). Pruébala con una función combinaciones(n, k) recursiva (C(n, k) = C(n-1, k-1) + C(n-1, k), con C(n, 0) = C(n, n) = 1) y cuenta cuántas llamadas reales se hacen con y sin memoización para combinaciones(25, 12).

Ejercicio 7: Puras frente a impuras

Reescribe estas funciones para que sean puras y explica qué efecto secundario tenía cada una:

let descuentoGlobal = 0.1;
function aplicarDescuento(precio) {
  return precio - precio * descuentoGlobal;
}

function marcarComoPagado(pedido) {
  pedido.pagado = true;
  return pedido;
}

Ejercicio 8: Función con opciones

Escribe formatearPrecio(cantidad, opciones) con el patrón de opciones: moneda (por defecto '€'), decimales (por defecto 2), simboloDelante (por defecto false) y separadorDecimal (por defecto ','). Ejemplos: formatearPrecio(1234.5) → '1234,50 €'; formatearPrecio(9.9, { moneda: '$', simboloDelante: true, separadorDecimal: '.' }) → '$9.90'. Documéntala con JSDoc completo.


Resumen

Concepto Sintaxis / idea clave
Declaración function sumar(a, b) { return a + b; } (con hoisting)
Expresión const restar = function (a, b) { return a - b; };
Flecha const doble = (n) => n * 2; (retorno implícito)
Devolver un objeto con flecha () => ({ clave: valor })
Por defecto function f(x = 10) {} (solo con undefined)
Rest function f(...args) {} → args es un array
arguments Legado; usa rest en su lugar
return Termina y devuelve; sin él, undefined
Callback Función pasada como argumento: repetir(3, (i) => ...)
Orden superior Recibe o devuelve funciones
Ámbito Global/módulo, función y bloque; se busca hacia fuera
Closure La función recuerda las variables de donde se creó
Memoización Caché de resultados guardada en un closure
IIFE (() => { ... })(); (histórico)
Recursión Caso base + llamada con un problema más pequeño
Función pura Mismo resultado con mismos argumentos y sin efectos secundarios
this Normal: el objeto antes del punto; flecha: el del entorno
Patrón de opciones function f({ a = 1, b = 2 } = {}) {}
JSDoc /** @param {tipo} nombre - desc @returns {tipo} */