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
returny 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
thisentre 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 pores,tieneopuede: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])da15.
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:
argumentsno existe en las funciones flecha y no es un array de verdad. En código moderno usa siempre parámetros rest (...args). Solo necesitas reconocerargumentscuando 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 (probablementeundefined).
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 llenarwindowde 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 sinawaitde 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:
- Un caso base: la condición que detiene la recursión.
- 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:
thises 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 tienenthispropio: usan elthisdel 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 necesitanthis, y funciones flecha para los callbacks dentro de esos métodos. Estudiarásthis,call,applyybinda 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 acrearBoton()sin argumentos lanzaTypeError: 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-checkal 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} */ |