La lección anterior terminó con una incomodidad concreta: describirTarea recibía ocho parámetros, los validadores recibían cuatro aunque cada uno usara solo uno, y no sabías qué ocurre cuando alguien llama a una función con menos argumentos de los declarados. Esta lección se ocupa exactamente de eso: cómo entran los datos en una función y cómo salen los resultados. Es la parte del diseño de funciones que más impacto tiene en la calidad del código, porque una firma bien pensada hace que la función se use bien sin necesidad de leer su interior. Al terminar sabrás dar valores por defecto, aceptar un número variable de argumentos, agrupar parámetros en un objeto de opciones, devolver varios valores a la vez y —lo más importante— distinguir una función pura de una que provoca efectos secundarios.
Contenido
- Argumentos de más y de menos
- Paso por valor: los primitivos
- Paso por valor de la referencia: objetos y arrays
- El aviso sobre mutar los argumentos
- Parámetros por defecto
- Dependencias entre parámetros por defecto
- El objeto
arguments(legado) - Parámetros rest
...args - El patrón "objeto de opciones"
return: devolver pronto y devolver un solo tipo- Devolver varios valores
- Funciones puras y efectos secundarios
- Caso práctico:
crearTareayresumirCarga - Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Argumentos de más y de menos
En JavaScript, el número de argumentos que pasas no tiene por qué coincidir con el número de parámetros declarados. No hay error, ni aviso.
function describirCarga(responsable, horas, limite) {
console.log(`${responsable} · ${horas} h · límite ${limite}`);
}
describirCarga('Iván', 25, 40); // Iván · 25 h · límite 40
describirCarga('Iván', 25); // Iván · 25 h · límite undefined
describirCarga('Iván'); // Iván · undefined h · límite undefined
describirCarga('Iván', 25, 40, 'x'); // Iván · 25 h · límite 40 ← el cuarto se ignoraLas reglas son sencillas:
| Situación | Qué ocurre |
|---|---|
| Faltan argumentos | Los parámetros sobrantes valen undefined |
| Sobran argumentos | Se ignoran (pero siguen accesibles, ver apartado 7) |
| Argumentos en otro orden | Se asignan por posición, no por nombre: silencio y resultados absurdos |
Esa permisividad es cómoda y peligrosa a partes iguales. El daño real aparece cuando el undefined se cuela en un cálculo:
function margenSemanal(horasAsignadas, limite) {
return limite - horasAsignadas;
}
console.log(margenSemanal(25)); // NaN ← undefined - 25Un NaN que se propaga por todos los cálculos siguientes y no da error hasta mucho después. Las dos defensas contra esto son los parámetros por defecto (apartado 5) y la validación explícita que aprendiste en Manejo de Errores.
- Paso por valor: los primitivos
Cuando pasas un primitivo (número, cadena, booleano, null, undefined, símbolo, bigint), la función recibe una copia del valor. Modificarlo dentro no afecta a la variable de fuera.
let horasIvan = 25;
function anyadirHoras(horas, extra) {
horas = horas + extra; // modifica la COPIA local
return horas;
}
const nuevoTotal = anyadirHoras(horasIvan, 5);
console.log(horasIvan); // 25 ← intacta
console.log(nuevoTotal); // 30flowchart LR
A["horasIvan<br/>25"] -->|copia el valor| B["parámetro horas<br/>25 → 30"]
B -.->|no vuelve| A
Esto es una garantía muy valiosa: una función nunca puede cambiar tus números y tus textos por sorpresa. Lo que hagas con ellos dentro se queda dentro.
- Paso por valor de la referencia: objetos y arrays
Con objetos y arrays la cosa cambia, y es la fuente número uno de errores sutiles. Recuerda lo que viste en Variables y Tipos de Datos: una variable que contiene un objeto no guarda el objeto, guarda una referencia (una dirección) al objeto.
Al llamar a una función, se copia esa referencia. La copia y el original apuntan al mismo objeto.
const tarea = {
id: 1,
titulo: 'Rediseñar la sala polivalente',
estado: 'en-curso',
horasEstimadas: 12
};
function marcarComoHecha(t) {
t.estado = 'hecha'; // ← modifica el objeto COMPARTIDO
}
marcarComoHecha(tarea);
console.log(tarea.estado); // 'hecha' ← ¡ha cambiado fuera!Pero reasignar el parámetro no afecta a nada de fuera, porque solo cambias a dónde apunta la copia local:
function reemplazar(t) {
t = { id: 99, titulo: 'Otra cosa' }; // la copia local apunta a otro objeto
}
reemplazar(tarea);
console.log(tarea.id); // 1 ← intactaflowchart TD
subgraph M["Memoria"]
O["{ id: 1, titulo: '…', estado: 'en-curso' }"]
O2["{ id: 99, titulo: 'Otra cosa' }"]
end
V["const tarea"] --> O
P1["parámetro t<br/>(mutación: t.estado = …)"] --> O
P2["parámetro t<br/>(reasignación: t = …)"] --> O2
Resumen en una tabla, porque esta distinción hay que tenerla automatizada:
| Operación dentro de la función | ¿Afecta fuera? |
|---|---|
t.estado = 'hecha' (mutar una propiedad) |
Sí |
lista.push(nueva) (mutar un array) |
Sí |
t = { ... } (reasignar el parámetro) |
No |
lista = [] (reasignar el parámetro) |
No |
n = n + 1 con un número |
No |
La frase técnica correcta es que JavaScript pasa siempre por valor; lo que ocurre es que, en el caso de los objetos, el valor que se copia es una referencia.
- El aviso sobre mutar los argumentos
Mutar un objeto recibido por parámetro es legal, a veces útil y casi siempre una mala idea si no está anunciado en el nombre de la función. El motivo es que rompe la expectativa de quien llama:
// ✗ Sorpresa: el nombre dice "calcular", pero además modifica
function calcularUrgencia(tarea, hoy) {
tarea.urgencia = pesoDePrioridad(tarea.prioridad) * 10; // efecto oculto
return tarea.urgencia;
}Quien lee const u = calcularUrgencia(t, HOY); no espera que t haya cambiado. Y si más adelante se guarda t en el almacenamiento del navegador, aparecerá un campo urgencia que nadie ha pedido.
Tres formas de hacerlo bien:
// ✓ 1. No mutar: solo calcular y devolver
function calcularUrgencia(prioridad, diasRestantes) {
return pesoDePrioridad(prioridad) * 10 - diasRestantes;
}
// ✓ 2. Devolver una copia modificada
function conEstado(tarea, nuevoEstado) {
const copia = Object.assign({}, tarea); // copia superficial
copia.estado = nuevoEstado;
return copia;
}
// ✓ 3. Mutar, pero anunciándolo en el nombre
function aplicarEstadoEnTarea(tarea, nuevoEstado) {
tarea.estado = nuevoEstado;
}La opción 2 es la que usaremos por defecto en Nómada Tareas. Las copias de objetos —incluida la diferencia entre copia superficial y profunda, que aquí solo mencionamos— se estudian en JSON y Copias de Objetos.
- Parámetros por defecto
Desde ES2015 puedes dar un valor a un parámetro para cuando no se pase (o se pase undefined):
function crearResumen(titulo, responsable = 'sin asignar', horas = 0) {
return `${titulo} · ${responsable} · ${horas} h`;
}
console.log(crearResumen('Cartelería del taller de serigrafía'));
// Cartelería del taller de serigrafía · sin asignar · 0 h
console.log(crearResumen('Cartelería del taller de serigrafía', 'Marta', 6));
// Cartelería del taller de serigrafía · Marta · 6 hDetalle crítico: el valor por defecto se aplica solo con undefined, no con cualquier valor falsy.
console.log(crearResumen('Inventario', undefined, 0)); // Inventario · sin asignar · 0 h
console.log(crearResumen('Inventario', null, 0)); // Inventario · null · 0 h
console.log(crearResumen('Inventario', '', 0)); // Inventario · · 0 hCompara con el patrón antiguo, que sí reaccionaba a todos los falsy y por eso daba problemas:
| Técnica | Se activa con | Problema |
|---|---|---|
horas = horas || 8 |
0, '', NaN, false, null, undefined |
Un 0 legítimo se convierte en 8 |
horas = horas ?? 8 |
null, undefined |
Correcto en la mayoría de casos |
function f(horas = 8) |
Solo undefined |
El más preciso; el que debes usar |
Los valores por defecto se evalúan en cada llamada, no una vez al definir la función. Eso permite usar expresiones:
function registrarCambio(tareaId, fecha = new Date().toISOString().slice(0, 10)) {
console.log(`[${fecha}] Cambio en la tarea ${tareaId}`);
}Cada llamada recalcula la fecha. Si el valor por defecto se hubiera evaluado una sola vez, todas las llamadas compartirían la misma fecha (que es precisamente el error clásico en otros lenguajes).
- Dependencias entre parámetros por defecto
Un parámetro por defecto puede usar los parámetros declarados a su izquierda:
function planificar(horasEstimadas, horasPorDia = 8, dias = Math.ceil(horasEstimadas / horasPorDia)) {
return `${horasEstimadas} h → ${dias} día(s) a ${horasPorDia} h/día`;
}
console.log(planificar(14)); // 14 h → 2 día(s) a 8 h/día
console.log(planificar(14, 4)); // 14 h → 4 día(s) a 4 h/día
console.log(planificar(14, 4, 1)); // 14 h → 1 día(s) a 4 h/díaAl revés no funciona, porque los parámetros se inicializan de izquierda a derecha:
function mal(dias = horas / 8, horas) { /* ... */ }
mal(undefined, 16); // ✗ ReferenceError: Cannot access 'horas' before initializationEsto es la misma zona muerta temporal (TDZ) de let/const aplicada a la lista de parámetros; el mecanismo completo está en Hoisting y el Contexto de Ejecución.
Regla práctica: coloca primero los parámetros obligatorios y después los que tienen valor por defecto. Si tienes que escribir f(dato, undefined, otro) para saltarte uno, la firma está mal diseñada y necesitas el patrón del apartado 9.
Un truco útil para exigir un parámetro sin escribir un if:
const obligatorio = (nombre) => {
throw new Error(`Falta el parámetro obligatorio: ${nombre}`);
};
function asignarTarea(tareaId = obligatorio('tareaId'), responsable = obligatorio('responsable')) {
return `Tarea ${tareaId} asignada a ${responsable}`;
}
console.log(asignarTarea(3, 'Lucía')); // Tarea 3 asignada a Lucía
asignarTarea(3); // ✗ Error: Falta el parámetro obligatorio: responsableFunciona porque el valor por defecto es una expresión que solo se evalúa si el argumento falta, y esa expresión lanza. Es fallar pronto, con un mensaje que dice exactamente qué falta.
- El objeto
arguments (legado)
arguments (legado)Dentro de cualquier función declarada con function existe una variable implícita llamada arguments con todos los argumentos recibidos, estén declarados o no:
function sumarHoras() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sumarHoras(12, 6, 14, 3, 8, 5)); // 48Ese 48 es el total de horas del backlog de Taller Nómada. Pero arguments tiene tres inconvenientes serios:
| Problema | Consecuencia |
|---|---|
| No es un array de verdad | Es array-like: tiene length e índices, pero ningún método de array |
| No existe en las flechas | () => arguments.length da ReferenceError o toma el de fuera |
| No aparece en la firma | Al leer function sumarHoras() parece que no recibe nada |
Ese tercer punto es el peor: la firma miente. Hoy arguments solo se usa al mantener código antiguo. Su sustituto es mejor en todo.
- Parámetros rest
...args
...argsLos parámetros rest recogen "todo lo demás" en un array de verdad:
function sumarHoras(...horas) {
let total = 0;
for (const h of horas) {
total += h;
}
return total;
}
console.log(sumarHoras(12, 6, 14, 3, 8, 5)); // 48
console.log(sumarHoras()); // 0Y se pueden combinar con parámetros normales, siempre en último lugar:
function informeDeCarga(responsable, ...horas) {
let total = 0;
for (const h of horas) total += h;
return `${responsable}: ${horas.length} tareas, ${total} h`;
}
console.log(informeDeCarga('Iván', 12, 8, 5)); // Iván: 3 tareas, 25 h
console.log(informeDeCarga('Lucía', 14)); // Lucía: 1 tareas, 14 h
console.log(informeDeCarga('Nadie')); // Nadie: 0 tareas, 0 hComparación directa:
arguments |
...rest |
|
|---|---|---|
| Tipo | Objeto array-like | Array real |
| Funciona en flechas | No | Sí |
| Visible en la firma | No | Sí |
| Selectivo | No, son todos | Sí, solo los no declarados |
| Métodos de array | No | Sí (Módulo 4) |
| Recomendado | Solo en código heredado | Siempre |
Los tres puntos
...aparecerán también como operador spread, que hace lo contrario: desplegar un array en argumentos sueltos. Ambos usos se ven juntos en Desestructuración de Objetos, Spread y Rest.
- El patrón "objeto de opciones"
Vuelve al problema con el que empezó la lección. Esta es la firma de describirTarea tal como quedó en 03-01:
describirTarea(1, 'Rediseñar la sala polivalente', 'Iván', 'alta', 'en-curso', 12, '2026-09-30', HOY);Ocho argumentos posicionales. Los problemas son evidentes:
- Nadie recuerda el orden. ¿Iba antes la prioridad o el estado?
- Un cambio de orden en la definición rompe todas las llamadas sin dar error.
- Para omitir el sexto hay que pasar
undefinedexplícito. - La llamada, leída sola, no dice qué significa cada valor.
El objeto de opciones consiste en pasar un único objeto cuyas claves nombran cada dato:
function describirTarea(opciones) {
const marca = opciones.estado === 'hecha' ? '✓' : '○';
const aviso = opciones.fechaLimite < opciones.hoy && opciones.estado !== 'hecha'
? ' ⚠ VENCIDA'
: '';
return `${marca} [${opciones.id}] ${opciones.titulo} · ${opciones.responsable} · ${opciones.horasEstimadas} h${aviso}`;
}
console.log(describirTarea({
id: 6,
titulo: 'Presupuesto de la carpintería',
responsable: 'Iván',
prioridad: 'alta',
estado: 'pendiente',
horasEstimadas: 5,
fechaLimite: '2026-09-05',
hoy: '2026-09-20'
}));
// ○ [6] Presupuesto de la carpintería · Iván · 5 h ⚠ VENCIDAVentajas y coste, para que la decisión sea consciente:
| Parámetros posicionales | Objeto de opciones | |
|---|---|---|
| Legibilidad en la llamada | Baja con más de 3 | Alta: cada valor va etiquetado |
| Orden | Obligatorio | Irrelevante |
| Omitir uno del medio | undefined explícito |
Simplemente no se pone |
| Añadir un dato nuevo | Rompe o alarga la firma | Añadir una clave más |
| Verbosidad | Menor | Mayor (hay que escribir las claves) |
| Errores de escritura en claves | No aplica | undefined silencioso |
Cuándo usar cada uno: con uno o dos parámetros, posicionales siempre. Con tres, depende. Con cuatro o más, o si hay varios opcionales del mismo tipo, objeto de opciones.
Y una variante muy habitual: parámetros obligatorios posicionales + un objeto final de opciones.
function formatearBacklog(tareas, opciones = {}) {
const separador = opciones.separador ?? '\n';
const mostrarHoras = opciones.mostrarHoras ?? true;
const orden = opciones.orden ?? 'fecha';
// ...
}
formatearBacklog(backlog); // todo por defecto
formatearBacklog(backlog, { orden: 'prioridad' }); // solo cambia lo que interesa
formatearBacklog(backlog, { mostrarHoras: false, orden: 'responsable' });Fíjate en opciones = {}: sin ese valor por defecto, llamar a formatearBacklog(backlog) daría un TypeError al intentar leer opciones.separador de undefined. En Desestructuración de Objetos verás cómo escribir esto de forma mucho más compacta.
return: devolver pronto y devolver un solo tipo
return: devolver pronto y devolver un solo tipo10.1 Devolver pronto
Ya lo aplicaste en 03-01 con las guardas. La regla es: trata los casos excepcionales al principio y sal; deja el caso normal al final, sin anidar.
// ✗ Anidamiento en escalera
function calcularMargen(responsable, horasAsignadas, limite) {
if (responsable !== null) {
if (typeof horasAsignadas === 'number') {
if (horasAsignadas <= limite) {
return limite - horasAsignadas;
} else {
return 0;
}
}
}
}
// ✓ Guardas y salida temprana
function calcularMargen(responsable, horasAsignadas, limite) {
if (responsable === null) return limite;
if (typeof horasAsignadas !== 'number') return limite;
if (horasAsignadas >= limite) return 0;
return limite - horasAsignadas;
}La segunda versión tiene la misma lógica, un nivel de indentación y —detalle importante— ninguna rama sin return. La primera devuelve undefined si responsable es null, algo que nadie escribió a propósito.
10.2 Devolver siempre el mismo tipo
Una función que a veces devuelve un número, a veces una cadena y a veces false obliga a quien la usa a comprobar el tipo cada vez:
// ✗ Tres tipos distintos según el caso
function buscarHoras(responsable) {
if (responsable === null) return 'sin responsable';
if (responsable === 'Iván') return 25;
return false;
}
const h = buscarHoras('Marta');
console.log(h + 10); // 10 ← false + 10, sin error y sin sentidoAlternativas correctas, por orden de preferencia:
| Estrategia | Ejemplo | Cuándo |
|---|---|---|
| Un valor neutro del mismo tipo | return 0; |
Cuando "nada" tiene un equivalente numérico natural |
null explícito |
return null; |
Cuando la ausencia es un resultado legítimo |
| Lanzar un error | throw new Error(...) |
Cuando la situación indica un fallo real |
| Un objeto resultado | { ok: false, motivo: '…' } |
Cuando quien llama necesita saber por qué |
function buscarHoras(responsable, responsables, horas) {
if (responsable === null) return null; // ausencia declarada
let total = 0;
let encontrado = false;
for (let i = 0; i < responsables.length; i++) {
if (responsables[i] === responsable) {
total += horas[i];
encontrado = true;
}
}
return encontrado ? total : null;
}Ahora quien llama sabe que recibe un número o null, y puede usar ?? 0 para el caso ausente.
- Devolver varios valores
Una función solo puede devolver un valor. Pero ese valor puede ser un contenedor. Hay dos opciones:
// Opción A: objeto (recomendado cuando los datos tienen nombre)
function resumirCarga(horas, estados) {
let total = 0;
let abiertas = 0;
let hechas = 0;
for (let i = 0; i < horas.length; i++) {
total += horas[i];
if (estados[i] === 'hecha') hechas += horas[i];
else abiertas += horas[i];
}
return { total: total, abiertas: abiertas, hechas: hechas };
}
const r = resumirCarga([12, 6, 14, 3, 8, 5],
['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente']);
console.log(r.total, r.abiertas, r.hechas); // 48 45 3// Opción B: array (solo cuando el orden es evidente y son pocos)
function rangoDeFechas(fechas) {
let minima = fechas[0];
let maxima = fechas[0];
for (const f of fechas) {
if (f < minima) minima = f;
if (f > maxima) maxima = f;
}
return [minima, maxima];
}
const rango = rangoDeFechas(['2026-09-30', '2026-10-15', '2026-09-05', '2026-11-05']);
console.log(rango[0], rango[1]); // 2026-09-05 2026-11-05| Objeto | Array | |
|---|---|---|
| Los datos van etiquetados | Sí | No, por posición |
| Orden significativo | No | Sí |
| Añadir un dato después | Fácil, no rompe nada | Rompe las posiciones |
| Cuándo | Casi siempre | 2-3 valores con orden natural (mín/máx, x/y) |
Leer r.total, r.abiertas, rango[0], rango[1] funciona pero es verboso. Existe una sintaxis mucho más cómoda para desempaquetar estos resultados —desestructuración— que aprenderás en Desestructuración de Arrays y Desestructuración de Objetos:
// Adelanto: así se escribirá a partir del Módulo 4
const { total, abiertas, hechas } = resumirCarga(horas, estados);
const [minima, maxima] = rangoDeFechas(fechas);
- Funciones puras y efectos secundarios
Una función es pura si cumple dos condiciones:
- Con los mismos argumentos devuelve siempre el mismo resultado.
- No provoca ningún efecto secundario: no modifica nada de fuera, no imprime, no guarda, no lee la hora ni un valor aleatorio.
// ✓ PURA: solo depende de sus argumentos y solo devuelve
function pesoDePrioridad(prioridad) {
if (prioridad === 'alta') return 3;
if (prioridad === 'media') return 2;
if (prioridad === 'baja') return 1;
return 0;
}
// ✗ IMPURA: depende de una variable externa
let limiteSemanal = 40;
function margenDe(horas) {
return limiteSemanal - horas; // el resultado cambia si cambia limiteSemanal
}
// ✗ IMPURA: efecto secundario (escribe en consola)
function informar(responsable, horas) {
console.log(`${responsable}: ${horas} h`);
}
// ✗ IMPURA: depende del reloj
function diasHasta(fechaLimite) {
const hoy = new Date(); // resultado distinto cada día
return Math.ceil((new Date(fechaLimite) - hoy) / 86400000);
}Las tres impuras se arreglan igual: lo que la función necesita del exterior, entra por parámetro.
function margenDe(horas, limiteSemanal) {
return limiteSemanal - horas;
}
function diasHasta(fechaLimite, hoy) {
return Math.ceil((new Date(fechaLimite) - new Date(hoy)) / 86400000);
}
console.log(diasHasta('2026-09-30', '2026-09-20')); // 10
console.log(diasHasta('2026-09-05', '2026-09-20')); // -15 (vencida)Por eso el proyecto usa HOY = '2026-09-20' como constante y la pasa como argumento en lugar de leer el reloj dentro de cada función.
Ventajas concretas de las funciones puras:
| Ventaja | Por qué |
|---|---|
| Fáciles de probar | Un test es "dado X, espero Y"; sin preparar entorno ni simular nada (08-03) |
| Fáciles de razonar | Para entenderlas basta leer su cuerpo; no hay estado escondido |
| Reutilizables | Funcionan igual en consola, en el navegador y en Node |
| Memoizables | Su resultado se puede cachear sin riesgo (03-04) |
| Sin errores de orden | Da igual cuándo se llamen |
Esto no significa que los efectos secundarios sean malos: sin ellos un programa no imprimiría nada ni guardaría nada. La estrategia es concentrarlos: un núcleo grande de funciones puras que calculan, y una capa fina de funciones impuras que imprimen, guardan o pintan. Esa separación es la que hará que en el Módulo 8 puedas probar toda la lógica de Nómada Tareas sin abrir el navegador.
flowchart TD
A["Entrada: datos del formulario"] --> B["Núcleo puro<br/>validarTarea · crearTarea<br/>resumirCarga · calcularUrgencia"]
B --> C["Capa de efectos<br/>console.log · localStorage<br/>DOM · fetch"]
C --> D["Salida: pantalla, disco, red"]
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#fff3e0,stroke:#ef6c00
- Caso práctico:
crearTarea y resumirCarga
crearTarea y resumirCargaJuntamos todo lo de la lección en dos funciones que acompañarán al proyecto el resto del curso.
'use strict';
const HOY = '2026-09-20';
const obligatorio = (campo) => {
throw new Error(`Falta el campo obligatorio: ${campo}`);
};
let ultimoId = 0;
/**
* Crea una tarea válida a partir de un objeto de opciones.
* Aplica R1 (id correlativo), R5 (nace pendiente), R8 (responsable null),
* R9 (etiquetas en minúsculas y sin duplicados).
* Impura a propósito: consume el contador de ids.
*/
function crearTarea(opciones = {}) {
const titulo = opciones.titulo ?? obligatorio('titulo');
const horasEstimadas = opciones.horasEstimadas ?? obligatorio('horasEstimadas');
const fechaLimite = opciones.fechaLimite ?? obligatorio('fechaLimite');
const responsable = opciones.responsable ?? null; // R8
const prioridad = opciones.prioridad ?? 'media';
const etiquetas = normalizarEtiquetas(opciones.etiquetas ?? []); // R9
const revisor = opciones.revisor ?? null;
if (titulo.trim().length === 0) throw new Error('R2: el título no puede estar vacío.');
if (horasEstimadas <= 0 || horasEstimadas > 40) throw new Error('R3: horas fuera de rango (0-40).');
ultimoId = ultimoId + 1; // R1
return {
id: ultimoId,
titulo: titulo.trim(),
responsable: responsable,
prioridad: prioridad,
estado: 'pendiente', // R5
etiquetas: etiquetas,
horasEstimadas: horasEstimadas,
fechaLimite: fechaLimite,
revisor: revisor
};
}
/** Pura: minúsculas, sin espacios sobrantes y sin duplicados (R9). */
function normalizarEtiquetas(etiquetas) {
const limpias = [];
for (const etiqueta of etiquetas) {
const normalizada = String(etiqueta).trim().toLowerCase();
if (normalizada === '') continue;
let repetida = false;
for (let j = 0; j < limpias.length; j++) {
if (limpias[j] === normalizada) { repetida = true; break; }
}
if (!repetida) limpias.push(normalizada);
}
return limpias;
}
const nueva = crearTarea({
titulo: ' Renovar el torno de la carpintería ',
horasEstimadas: 10,
fechaLimite: '2026-10-20',
responsable: 'Lucía',
prioridad: 'alta',
etiquetas: ['Carpintería', 'carpintería', ' MANTENIMIENTO ']
});
console.log(nueva);
// {
// id: 1,
// titulo: 'Renovar el torno de la carpintería',
// responsable: 'Lucía',
// prioridad: 'alta',
// estado: 'pendiente',
// etiquetas: [ 'carpintería', 'mantenimiento' ],
// horasEstimadas: 10,
// fechaLimite: '2026-10-20',
// revisor: null
// }Repasa qué técnicas de la lección aparecen ahí: objeto de opciones con opciones = {} por defecto, ?? para los valores opcionales, la función obligatorio() como valor por defecto que lanza, validación con fallo temprano, retorno de un objeto nuevo (sin mutar opciones) y una función auxiliar pura (normalizarEtiquetas).
Ahora el resumen de carga por responsable, completamente puro:
/**
* Devuelve varios totales de un responsable en un solo objeto.
* Pura: mismos arrays y mismo responsable → mismo resultado.
*/
function resumirCarga(responsables, horas, estados, fechasLimite, responsable, hoy = HOY) {
let totalHoras = 0;
let horasAbiertas = 0;
let tareas = 0;
let vencidas = 0;
for (let i = 0; i < responsables.length; i++) {
if (responsables[i] !== responsable) continue;
tareas++;
totalHoras += horas[i];
if (estados[i] !== 'hecha') {
horasAbiertas += horas[i];
if (fechasLimite[i] < hoy) vencidas++;
}
}
return {
responsable: responsable,
tareas: tareas,
totalHoras: totalHoras,
horasAbiertas: horasAbiertas,
vencidas: vencidas,
margen: 40 - horasAbiertas, // R7
sobrecargado: horasAbiertas > 40 // R7
};
}
const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const horas = [12, 6, 14, 3, 8, 5];
const estados = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
const fechasLimite = ['2026-09-30', '2026-10-15', '2026-10-02',
'2026-09-12', '2026-11-05', '2026-09-05'];
const equipo = ['Iván', 'Marta', 'Lucía'];
for (const persona of equipo) {
const r = resumirCarga(responsables, horas, estados, fechasLimite, persona);
console.log(`${r.responsable}: ${r.tareas} tareas · ${r.horasAbiertas}/${r.totalHoras} h abiertas · ` +
`margen ${r.margen} h · vencidas ${r.vencidas}`);
}
// Iván: 3 tareas · 25/25 h abiertas · margen 15 h · vencidas 1
// Marta: 2 tareas · 6/9 h abiertas · margen 34 h · vencidas 0
// Lucía: 1 tareas · 14/14 h abiertas · margen 26 h · vencidas 0Siete datos en un solo retorno, y cero efectos secundarios: quien llama decide si imprimirlos, pintarlos en el Módulo 6 o compararlos. Esa es exactamente la separación núcleo puro / capa de efectos del diagrama anterior.
Errores Comunes y Consejos
1. Mutar un objeto recibido sin decirlo. Si tu función se llama calcular… u obtener…, no debe cambiar nada. Devuelve un valor nuevo.
2. Usar || para valores por defecto numéricos.
function conMargen(horas) {
horas = horas || 8; // ✗ un 0 legítimo se convierte en 8
return horas;
}
console.log(conMargen(0)); // 8 ← malUsa ?? o, mejor, un parámetro por defecto real.
3. Poner parámetros opcionales antes que obligatorios. Obliga a escribir undefined en las llamadas y es un olor a diseño mejorable.
4. Devolver undefined sin querer. Un if sin else y sin return final deja ramas mudas. Comprueba que todos los caminos devuelven algo.
5. Errata en una clave del objeto de opciones.
crearTarea({ titluo: 'Nueva', horasEstimadas: 4, fechaLimite: '2026-10-01' });
// ✗ Error: Falta el campo obligatorio: tituloEn este caso el obligatorio() lo detecta; en opciones no obligatorias, la errata pasa desapercibida y el valor por defecto se aplica en silencio. Es el precio del patrón, y la razón por la que conviene validar las claves aceptadas en funciones críticas.
6. Confundir rest con spread. ... en la definición recoge (rest); ... en la llamada despliega (spread). Ambos en 04-07.
7. Consejo: escribe la firma antes que el cuerpo. Si la firma no cabe cómodamente en una línea, replantéala antes de programar nada.
8. Consejo: tres parámetros es el umbral de alerta. A partir de ahí, pregúntate si esos datos en realidad forman una sola cosa (una tarea, unas opciones, un rango).
Ejercicios
Ejercicio 1 — Puro o impuro
Clasifica cada función como pura o impura y, si es impura, reescríbela para que sea pura sin cambiar lo que calcula.
// a)
const IVA = 0.21;
function conImpuestos(base) { return base * (1 + IVA); }
// b)
let contadorLlamadas = 0;
function urgencia(prioridad, dias) {
contadorLlamadas++;
return pesoDePrioridad(prioridad) * 10 - dias;
}
// c)
function ordenarEtiquetas(etiquetas) {
etiquetas.sort();
return etiquetas;
}
// d)
function diasDeMargen(fechaLimite) {
const hoy = new Date().toISOString().slice(0, 10);
return fechaLimite >= hoy ? 'a tiempo' : 'vencida';
}Ejercicio 2 — Rediseñar una firma
Esta función tiene una firma inmanejable. Rediséñala con objeto de opciones y valores por defecto, de modo que las tres llamadas del final funcionen.
function generarInforme(tareas, responsable, incluirHechas, ordenarPor, formato, ancho, mostrarVencidas) {
// ...
}
// Llamadas que deben funcionar tras el rediseño:
// 1) Solo el backlog, todo lo demás por defecto
// 2) El backlog de Iván, incluyendo las hechas
// 3) El backlog ordenado por prioridad, en formato compactoEjercicio 3 — resumirBacklog con rest y retorno múltiple
Escribe resumirBacklog(estados, horas, ...responsablesFiltro) que:
- Si no se pasa ningún responsable, resuma todo el backlog.
- Si se pasan uno o más nombres, resuma solo las tareas de esas personas.
- Devuelva un objeto con
tareas,horasTotales,horasAbiertasyporcentajeCompletado(horas hechas sobre el total, redondeado a un decimal).
Pruébala con el backlog completo, con 'Iván' y con 'Marta', 'Lucía'.
Soluciones
Ejercicio 1
| Caso | Veredicto | Motivo |
|---|---|---|
| a) | Pura en la práctica | Depende de IVA, pero es una constante inmutable del módulo. Aceptable; si IVA fuera let, sería impura |
| b) | Impura | Muta contadorLlamadas, una variable externa |
| c) | Impura | sort() ordena el array en el sitio: muta el argumento |
| d) | Impura | Lee el reloj: el resultado cambia con el día |
// b) Sin efecto secundario. Si hace falta contar, que lo haga quien llama.
function urgencia(prioridad, dias) {
return pesoDePrioridad(prioridad) * 10 - dias;
}
// c) Copiar antes de ordenar
function ordenarEtiquetas(etiquetas) {
const copia = [];
for (const e of etiquetas) copia.push(e);
copia.sort();
return copia;
}
const originales = ['serigrafía', 'diseño', 'carpintería'];
const ordenadas = ordenarEtiquetas(originales);
console.log(originales); // [ 'serigrafía', 'diseño', 'carpintería' ] ← intacto
console.log(ordenadas); // [ 'carpintería', 'diseño', 'serigrafía' ]
// d) La fecha entra por parámetro
function diasDeMargen(fechaLimite, hoy) {
return fechaLimite >= hoy ? 'a tiempo' : 'vencida';
}
console.log(diasDeMargen('2026-09-05', '2026-09-20')); // vencidaEjercicio 2
function generarInforme(tareas, opciones = {}) {
const responsable = opciones.responsable ?? null; // null = todo el equipo
const incluirHechas = opciones.incluirHechas ?? false;
const ordenarPor = opciones.ordenarPor ?? 'fecha';
const formato = opciones.formato ?? 'detallado';
const ancho = opciones.ancho ?? 80;
const mostrarVencidas = opciones.mostrarVencidas ?? true;
return `Informe [${responsable ?? 'equipo'}] · orden: ${ordenarPor} · formato: ${formato} · ` +
`${incluirHechas ? 'con' : 'sin'} hechas · ancho ${ancho}` +
`${mostrarVencidas ? ' · marcando vencidas' : ''}`;
}
console.log(generarInforme(backlog));
// Informe [equipo] · orden: fecha · formato: detallado · sin hechas · ancho 80 · marcando vencidas
console.log(generarInforme(backlog, { responsable: 'Iván', incluirHechas: true }));
// Informe [Iván] · orden: fecha · formato: detallado · con hechas · ancho 80 · marcando vencidas
console.log(generarInforme(backlog, { ordenarPor: 'prioridad', formato: 'compacto' }));
// Informe [equipo] · orden: prioridad · formato: compacto · sin hechas · ancho 80 · marcando vencidasComentario: tareas sigue siendo posicional porque es el dato principal y siempre está presente. Todo lo demás es configuración opcional y va en el objeto. Fíjate en que ninguna de las tres llamadas necesita escribir undefined ni recordar un orden.
Ejercicio 3
function resumirBacklog(estados, horas, ...responsablesFiltro) {
const filtrar = responsablesFiltro.length > 0;
let tareas = 0;
let horasTotales = 0;
let horasAbiertas = 0;
let horasHechas = 0;
for (let i = 0; i < estados.length; i++) {
if (filtrar) {
let coincide = false;
for (const nombre of responsablesFiltro) {
if (responsables[i] === nombre) { coincide = true; break; }
}
if (!coincide) continue;
}
tareas++;
horasTotales += horas[i];
if (estados[i] === 'hecha') horasHechas += horas[i];
else horasAbiertas += horas[i];
}
const porcentaje = horasTotales === 0
? 0
: Math.round((horasHechas / horasTotales) * 1000) / 10;
return {
tareas: tareas,
horasTotales: horasTotales,
horasAbiertas: horasAbiertas,
porcentajeCompletado: porcentaje
};
}
console.log(resumirBacklog(estados, horas));
// { tareas: 6, horasTotales: 48, horasAbiertas: 45, porcentajeCompletado: 6.3 }
console.log(resumirBacklog(estados, horas, 'Iván'));
// { tareas: 3, horasTotales: 25, horasAbiertas: 25, porcentajeCompletado: 0 }
console.log(resumirBacklog(estados, horas, 'Marta', 'Lucía'));
// { tareas: 3, horasTotales: 23, horasAbiertas: 20, porcentajeCompletado: 13 }Comentario: responsablesFiltro.length > 0 distingue "sin filtro" de "con filtro" sin necesidad de un parámetro extra. La guarda horasTotales === 0 evita el NaN de dividir entre cero, un caso que aparece en cuanto el filtro no encuentra a nadie. Y el truco Math.round(x * 1000) / 10 redondea a un decimal el porcentaje.
Conclusión
Ahora dominas los dos extremos de una función. Por la entrada: sabes que los argumentos se emparejan por posición, que los que faltan valen undefined y que los que sobran se ignoran; que los primitivos se pasan por valor y los objetos por valor de la referencia, con la consecuencia de que mutar una propiedad de un argumento sí se ve fuera mientras que reasignar el parámetro no; que los parámetros por defecto se activan solo con undefined, se evalúan en cada llamada y pueden depender de los parámetros anteriores; que ...args sustituye al viejo arguments con un array de verdad; y que a partir de tres o cuatro datos conviene pasar un objeto de opciones con opciones = {} por defecto.
Por la salida: sabes devolver pronto con guardas, mantener un único tipo de retorno, elegir entre null, un valor neutro o un throw para representar la ausencia, y empaquetar varios resultados en un objeto (o en un array cuando el orden es evidente), a la espera de la desestructuración del Módulo 4. Y tienes el criterio más importante de todos: una función pura —mismo resultado con los mismos argumentos, sin efectos secundarios— es fácil de probar, de razonar y de reutilizar, así que la estrategia es un núcleo puro grande y una capa fina de efectos. crearTarea() y resumirCarga() de Nómada Tareas son la aplicación directa de todo ello.
Queda un cabo suelto que has rozado varias veces sin nombrarlo. Cuando crearTarea hace ultimoId = ultimoId + 1, está tocando una variable que vive fuera de ella. ¿Por qué puede verla? ¿Dónde vive exactamente cada variable? ¿Qué ocurre si dentro de una función declaro otra variable con el mismo nombre? Y sobre todo: ¿se puede tener un contador de ids que nadie más pueda tocar por accidente, sin dejarlo suelto en el fichero? Las respuestas están en Ámbito y Closures, donde construirás crearGeneradorDeIds() y descubrirás que una función puede recordar el entorno en el que nació.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
