Guía de JavaScript: De 0 a 100

Guía de JavaScript: De 0 a 100

El lenguaje que mueve la web. Aprende JS desde los fundamentos hasta producción: event loop, closures, promesas, POO, patrones, bases de datos y microservicios.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:80 MINUTOS DE LECTURA

Guía de JavaScript: De 0 a 100

¿Qué es JavaScript?

JavaScript es un lenguaje de programación interpretado, de tipado dinámico y multiparadigma, creado por Brendan Eich en 1995 en Netscape. Originalmente diseñado para añadir interactividad al navegador, hoy es uno de los lenguajes más ubicuos del mundo.

Nacido como Mocha, luego LiveScript y finalmente JavaScript, en 1997 se estandarizó como ECMAScript (ES). Desde ES6 (2015) el lenguaje ha evolucionado enormemente con clases, promesas, módulos, y más.

¿Dónde se usa?

  • Frontend web: React, Vue, Svelte, Angular — el único lenguaje nativo del navegador
  • Backend: Node.js (Express, Fastify, NestJS), Deno, Bun — JavaScript en el servidor
  • Aplicaciones de escritorio: Electron (VS Code, Slack, Discord, Figma), Tauri
  • Aplicaciones móviles: React Native, Ionic, NativeScript, Expo
  • Bases de datos: MongoDB (aggregation pipeline), Firebase (Cloud Functions), CockroachDB
  • IoT: Johnny-Five, Espruino, Moddable SDK
  • CLI / herramientas: npm, yarn, eslint, vite, webpack, turborepo
  • Juegos: Phaser, Three.js, Babylon.js, PlayCanvas

¿Quién lo usa? Google (GMail, Maps, YouTube), Netflix (UI + Express backend), Facebook/Meta (React, Jest), Microsoft (VS Code, Teams, Edge), PayPal (Node.js backend), LinkedIn (backbone de la app), Figma (editor en WebAssembly + JS), MongoDB (shell y drivers).

Fuente: MDN — About JavaScript, ECMAScript spec, Node.js — About


Prerrequisitos

Antes de empezar, necesitas:

  • Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
  • El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)

Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.

¿Cómo empezar? Instalación en todos los entornos

Abre Chrome, Edge o Firefox → F12 → pestaña Console. Ya tienes un REPL de JavaScript. Escribe console.log("Hola mundo").

Node.js (estándar para backend y herramientas)

# Windows — descargar de https://nodejs.org/ (LTS)
# O con winget:
winget install OpenJS.NodeJS.LTS

# macOS
brew install node@22

# Linux (Debian/Ubuntu)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install nodejs

# Verificar
node --version  # v22.x
npm --version   # 10.x

nvm (gestor de versiones de Node)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# Recargar shell o:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

nvm install 22    # Última LTS
nvm install 20    # LTS anterior
nvm install 18    # Aún con soporte
nvm alias default 22
nvm use 22

Docker

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Usa node:22-alpine (~130 MB). Para desarrollo, node:22-slim incluye herramientas de compilación.

Bun (alternativa ultrarrápida)

curl -fsSL https://bun.sh/install | bash
bun --version
bun init
bun run index.ts  # Bun ejecuta TypeScript nativamente

Bun es un runtime todo-en-uno (intérprete, bundler, package manager, test runner). Compatible con la mayoría de APIs de Node.js.

Deno (seguro por defecto, TypeScript nativo)

curl -fsSL https://deno.land/install.sh | sh
deno --version
deno run https://deno.land/std/examples/welcome.ts

Deno implementa web APIs estándar (fetch, WebSocket) de forma nativa. No usa package.json — importa desde URLs.

Alias útiles para el día a día

Añade a tu ~/.bashrc o ~/.zshrc:

alias node='node --experimental-strip-types'  # Ejecuta TS sin ts-node
alias nrd='npm run dev'
alias nrb='npm run build'
alias nrt='npm test'
alias ni='npm install'
alias nrs='npm run start'
alias ncu='npx npm-check-updates -u'  # Actualiza dependencias
alias pn='pnpm'
alias b='bun'

Fuente: nvm GitHub, Node.js Docker images, Bun docs, Deno docs


Escala de aprendizaje: de 0 a 100

Nivel 0–15: Fundamentos absolutos

Que aprender:

  • Variables (let/const), tipos primitivos, console.log()
  • Operadores: +, -, *, /, ===, !==, &&, ||, !
  • Condicionales: if/else, ternario
  • Bucles: for, for...of, while
  • Arrays: push(), pop(), length, acceso por indice, includes()
  • Funciones: function declaration, arrow functions

Proyecto: Adivina el numero — numero aleatorio, pistas mayor/menor, contador de intentos.

Nivel 15–30: Funciones, objetos y DOM

Que aprender:

  • Objetos literales, propiedades dinámicas, shorthand
  • this, call, apply, bind
  • Spread, destructuring
  • map(), filter(), reduce(), find(), sort()
  • Closures
  • TypedArrays, Symbol, BigInt

Proyecto: Gestor de tareas CLI — añadir, listar, completar, eliminar con persistencia JSON.

Nivel 30–45: POO, prototipos y asincronia básica

Que aprender:

  • Clases ES6+, extends, super(), campos privados #
  • Promesas, Promise.all, async/await básico
  • File system: fs.promises, path
  • Canvas 2D básico

Proyecto: Sistema de inventario — clases Producto, Inventario, persistencia asíncrona.

Nivel 45–60: Testing, módulos y herramientas

Que aprender:

  • ESM: import/export
  • node:test o vitest
  • ESLint + Prettier
  • TypeScript básico
  • Streams, Buffer, fs.watch
  • Sharp para imágenes

Proyecto: Calculadora empaquetada — paquete npm ESM, tests, CI.

Nivel 60–75: APIs y async avanzado

Que aprender:

  • Express/Fastify, middleware, rutas
  • Prisma/Drizzle, migrations, CRUD
  • Zod para validación
  • JWT para autenticación
  • Testing de APIs con Supertest

Proyecto: API de recetas — CRUD, autenticación JWT, validación Zod, Prisma SQLite.

Nivel 75–90: Producción y tiempo real

Que aprender:

  • Docker, docker-compose (app + db + redis)
  • WebSockets (Socket.IO), Redis, BullMQ
  • Logging con pino/winston
  • Rate limiting, CORS, Helmet

Proyecto: Dashboard de monitorización — tiempo real con WebSocket, Redis, Docker Compose.

Nivel 90–100: Arquitectura y microservicios

Que aprender:

  • Microservicios, API Gateway
  • Kafka/RabbitMQ, gRPC, GraphQL
  • OpenTelemetry, Prometheus, Grafana
  • Patrones: Circuit Breaker, Retry, Saga
  • TypeScript avanzado

Proyecto: Sistema de notificaciones distribuido — microservicios, Kafka, Docker, OpenTelemetry.

🔗 Para saber más


Primeros pasos y configuración del entorno

Si prefieres ver directamente el “Hola Mundo” en JavaScript, consulta El Atlas del Hola Mundo.

El REPL de Node.js

node
> console.log("Hola mundo")
Hola mundo
> 2 + 2
4
> [1, 2, 3].map(x => x ** 2)
[ 1, 4, 9 ]
> .exit

Hola mundo (módulo ESM)

// hola.mjs — extensión .mjs para ES Modules explícito
function main() {
  console.log("Hola mundo");
}

main();

Ejecuta: node hola.mjs

O en package.json añade "type": "module" y usa .js:

{
  "type": "module",
  "name": "mi-proyecto",
  "version": "1.0.0"
}

Crear un proyecto npm

mkdir mi-proyecto && cd mi-proyecto
npm init -y                    # package.json básico
npm install express            # instalar dependencia
npm install -D typescript      # dev dependency
npx tsc --init                 # inicializar tsconfig.json

Editores recomendados

Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.

VSCode y herramientas de calidad

Extensiones esenciales:

  • ESLint (Microsoft) — linting de JS/TS
  • Prettier (Prettier) — formateador automático
  • JavaScript and TypeScript Nightly (Microsoft) — últimas features del language server
  • npm Intellisense (Christian Kohler) — autocompletado en imports

Configuración mínima eslint.config.js (flat config, ESLint 9+):

import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    rules: {
      "no-unused-vars": "warn",
      "no-console": "off",
      "eqeqeq": ["error", "always"],
      "curly": "error",
      "semi": ["error", "always"],
    },
  },
];

Configuración .prettierrc:

{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100
}

TypeScript (tsconfig.json mínimo)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Ejecuta con npx tsc --watch o tsx (ejecución directa de TS): npx tsx src/index.ts.

Fuente: Node.js — Modules: Packages, ESLint docs, Prettier docs, TypeScript tsconfig


Paradigmas de programación

JavaScript es un lenguaje multiparadigma: puedes escribir código imperativo, orientado a objetos, funcional, event-driven y reactivo, a menudo combinándolos en un mismo programa. Su identidad real es la de un lenguaje event-driven con fuerte influencia funcional.

Programación imperativa

Describir paso a paso cómo se hace algo:

const numeros = [3, 1, 4, 1, 5, 9];
let suma = 0;
for (let i = 0; i < numeros.length; i++) {
  suma += numeros[i];
}
console.log(suma); // 23

Programación orientada a objetos (prototipos y clases)

JS implementa POO mediante prototipos (no clases clásicas al estilo Java). Las clases ES6+ son azúcar sintáctico sobre prototipos:

// Prototipos
function Animal(nombre) {
  this.nombre = nombre;
}
Animal.prototype.saludar = function () {
  return `Hola, soy ${this.nombre}`;
};

const perro = new Animal("Rex");
console.log(perro.saludar()); // "Hola, soy Rex"

// Clases (mismo mecanismo interno)
class Gato extends Animal {
  constructor(nombre) {
    super(nombre);
  }
  maullar() {
    return "Miau";
  }
}

const gato = new Gato("Bigotes");
console.log(gato.saludar());       // "Hola, soy Bigotes"
console.log(typeof Gato);           // "function"
console.log(Gato.prototype === Object.getPrototypeOf(gato)); // true

Programación funcional (callbacks, closures, map/filter/reduce)

JS trata las funciones como ciudadanos de primera clase: se asignan, pasan como argumento y se devuelven desde otras funciones.

Callbacks:

function procesar(datos, callback) {
  return datos.map(callback);
}

console.log(procesar([1, 2, 3], (x) => x * 2)); // [2, 4, 6]

setTimeout(() => console.log("Temporizador"), 1000);

Closures (función que recuerda su ámbito de creación):

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

const duplicar = crearMultiplicador(2);
const triplicar = crearMultiplicador(3);
console.log(duplicar(5));  // 10
console.log(triplicar(5)); // 15

// Uso real: función con estado encapsulado
function crearContador(inicial = 0) {
  let cuenta = inicial;
  return {
    incrementar: () => ++cuenta,
    decrementar: () => --cuenta,
    obtener: () => cuenta,
  };
}

const c = crearContador(10);
console.log(c.incrementar()); // 11
console.log(c.obtener());     // 11

map, filter, reduce — las tres operaciones fundamentales:

const nums = [1, 2, 3, 4, 5, 6];

const pares = nums.filter((n) => n % 2 === 0);      // [2, 4, 6]
const dobles = nums.map((n) => n * 2);               // [2, 4, 6, 8, 10, 12]
const total = nums.reduce((acc, n) => acc + n, 0);   // 21

// Composición funcional (pipeline):
const resultado = nums
  .filter((n) => n % 2 === 0)     // [2, 4, 6]
  .map((n) => n * 2)               // [4, 8, 12]
  .reduce((acc, n) => acc + n, 0); // 24

Funciones puras e inmutabilidad:

// Impura (modifica el original)
const agregarImpuro = (arr, item) => {
  arr.push(item);
  return arr;
};

// Pura (devuelve nuevo array)
const agregarPuro = (arr, item) => [...arr, item];

const original = [1, 2];
const resultado = agregarPuro(original, 3);
console.log(original);  // [1, 2] — sin modificar
console.log(resultado); // [1, 2, 3]

Programación event-driven

El núcleo de JavaScript en el navegador y Node.js está basado en eventos gestionados por el event loop:

// Navegador
document.querySelector("button")
  .addEventListener("click", (event) => {
    console.log("Clickeado", event);
  });

// Node.js (EventEmitter nativo)
import { EventEmitter } from "events";

const emisor = new EventEmitter();
emisor.on("datos", (payload) => console.log("Recibido:", payload));
emisor.emit("datos", { id: 1, mensaje: "hola" });

Programación reactiva (RxJS)

Extiende el patrón observer con operadores para componer flujos de datos asíncronos:

import { fromEvent, map, filter, debounceTime } from "rxjs";

const input = document.querySelector("#buscador");

fromEvent(input, "input").pipe(
  debounceTime(300),
  map((e) => e.target.value),
  filter((texto) => texto.length >= 3),
).subscribe((query) => {
  console.log("Buscar:", query);
});

Tabla comparativa de paradigmas

ParadigmaUnidad básicaEstadoEfectos secundariosSoporte JS
ImperativoInstruccionesMutableSíNativo
POO (prototipos)ObjetosPropiedades mutablesSíNativo
POO (clases)Clases/instanciasPropiedades mutablesSíES6+
FuncionalFuncionesInmutable (preferido)EvitadosNativo
Event-drivenEventosCallbacks/ListenersSíNativo
ReactivoStreams/ObservablesFlujo de datosGestionadoRxJS

JS destaca por combinar event-driven (el event loop) con funcional (closures, map/filter/reduce). No hay que elegir uno — se mezclan constantemente.

Fuente: MDN — Introduction, RxJS docs, Exploring JS


Tipos de datos y variables

typeof y tipado dinámico

¿Qué es?

JavaScript tiene tipado dinámico: una variable puede cambiar de tipo en cualquier momento. No declaras el tipo — el intérprete lo determina en tiempo de ejecución. Usa typeof para inspeccionar el tipo actual.

  • Analogía: es como una caja que puede contener cualquier cosa, y cambia de contenido sin avisar.
  • 🔗 MDN — typeof

Sintaxis básica

typeof 42;             // "number"
typeof "hola";         // "string"
typeof true;           // "boolean"
typeof undefined;      // "undefined"
typeof null;           // "object" — bug histórico (no se corrigió por retrocompatibilidad)
typeof Symbol();       // "symbol"
typeof 42n;            // "bigint"
typeof {};             // "object"
typeof [];             // "object"
typeof (() => {});     // "function"
typeof NaN;            // "number" — NaN es de tipo number

🧪 Cómo probarlo

node -e "console.log(typeof 42, typeof 'hola', typeof true, typeof undefined, typeof null)"
// Test con node:test (Node 20+)
import assert from "node:assert/strict";
assert.strictEqual(typeof 42, "number");
assert.strictEqual(typeof "hola", "string");
assert.strictEqual(typeof null, "object");

💡 Memoria y rendimiento

  • typeof es una operación O(1) — el tipo se almacena en los bits del valor (V8 usa puntero con tag)
  • No hay penalización por usar typeof en bucles estrechos
  • ⚠️ typeof null === "object" es un bug de JS que no se puede corregir por retrocompatibilidad

✅ Buenas prácticas

  • Usa typeof x === "undefined" para verificar si una variable existe (seguro incluso si no está declarada)
  • Para detectar arrays: Array.isArray(x) — typeof devuelve “object”
  • Para detectar null: x === null
  • ❌ No uses typeof para detectar objetos: typeof null da “object”

🏗️ Metodología

  • Usa typeof para type guards en tiempo de ejecución (ej: aceptar string o número en una función)
  • Alternativa moderna: TypeScript aporta tipos estáticos y typeof en tiempo de tipo
  • En APIs públicas, valida tipos al inicio para dar errores claros

🔗 Para saber más

Primitivos: number, bigint, string, boolean, symbol, null, undefined

¿Qué es?

JavaScript tiene 7 tipos primitivos: number, bigint, string, boolean, symbol, null, undefined. Los primitivos son inmutables y se almacenan por valor en el stack.

Sintaxis básica

// Number (IEEE 754 double-precision)
const entero = 42;
const decimal = 3.14;
const notacion = 1.5e6;    // 1500000
const binario = 0b1010;    // 10
const octal = 0o77;        // 63
const hex = 0xFF;          // 255
const separador = 1_000_000; // 1000000

// Problemas de precisión
0.1 + 0.2;                 // 0.30000000000000004
0.1 + 0.2 === 0.3;         // false

// BigInt (para enteros > 2^53 - 1)
const grande = 9007199254740993n;
const enorme = BigInt("123456789012345678901234567890");
5n / 2n;                   // 2n (trunca)

// String (UTF-16)
const simple = "hola";
const doble = 'mundo';
const template = `Hola ${simple}`;
const emoji = "😀";
emoji.length;              // 2 — ¡no 1! (ocupa 2 unidades UTF-16)
[...emoji].length;         // 1 — spread itera por caracteres Unicode

// Boolean
const verdad = true;
const falso = false;

// Symbol (único)
const ID = Symbol("id");
Symbol("id") === Symbol("id"); // false

// null (ausencia intencional) vs undefined (no asignado)
let a;
console.log(a);        // undefined
const b = null;
console.log(b);        // null

// NaN — único valor que no es igual a sí mismo
NaN === NaN;           // false
Number.isNaN(NaN);     // true

🧪 Cómo probarlo

node -e "
console.log(typeof 42n);         // bigint
console.log(0.1 + 0.2 === 0.3); // false
console.log(NaN === NaN);        // false
console.log([...'😀'].length);   // 1
"
import assert from "node:assert/strict";
assert.strictEqual(typeof 42n, "bigint");
assert.strictEqual(Number.isNaN(NaN), true);
assert.strictEqual([..."😀"].length, 1);

💡 Memoria y rendimiento

TipoTamañoStack/HeapMutabilidad
number8 bytes (double)StackInmutable
bigintVariableHeapInmutable
string2 bytes x char (UTF-16)Heap (internado)Inmutable
boolean4 bytesStackInmutable
symbol16-32 bytesHeapInmutable
null0 (referencia nula)Stack—
undefined0Stack—
  • Number sigue IEEE 754: enteros exactos hasta ±2^53 (≈9e15)
  • Strings inmutables: cualquier operación crea un nuevo string
  • ⚠️ Concatenar muchos strings en bucle es O(n²) — usar array .join() o template literals

✅ Buenas prácticas

  • Usa Number.EPSILON para comparar floats: Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON
  • Para moneda, usa céntimos (integer) o librería como decimal.js
  • Detecta NaN con Number.isNaN() no con isNaN() (que coerce)
  • ❌ No mezcles BigInt con Number: 1n + 1 lanza TypeError
  • ❌ No confíes en == para null checks: usa x === null || x === undefined o x == null

🏗️ Metodología

  • Usa const por defecto, let cuando necesites reasignar, nunca var en código moderno
  • Para cálculos financieros: usa BigInt con céntimos o decimal.js
  • Para datos binarios: TypedArrays (más eficientes que manipular strings)

🔗 Para saber más

Object y Array

¿Qué es?

Los objetos son colecciones dinámicas de pares clave-valor (claves son strings o Symbols). Los arrays son objetos especializados con índices numéricos y propiedad length. No son arrays contiguos en memoria como en C/Rust — son diccionarios con índice como string.

Sintaxis básica

// Objetos
const obj = {
  nombre: "Ana",
  edad: 30,
  saludar() { return `Hola, soy ${this.nombre}`; },
};

// Propiedades dinámicas
const clave = "email";
obj[clave] = "ana@email.com";

// Shorthand
const x = 1, y = 2;
const punto = { x, y };

// Métodos estáticos
Object.keys(obj);
Object.values(obj);
Object.entries(obj);
Object.fromEntries([["a", 1]]);
Object.freeze(obj);       // shallow
Object.seal(obj);         // impide añadir/borrar, permite modificar

// Descriptores
Object.defineProperty(obj, "fijo", {
  value: 42,
  writable: false,
  enumerable: true,
  configurable: false,
});

// Arrays
const arr = [10, 20, 30];

arr.push(2);              // añade al final
arr.pop();                // quita del final
arr.unshift(0);           // añade al inicio
arr.shift();              // quita del inicio
arr.splice(2, 1);         // elimina 1 en indice 2
arr.slice(1, 3);          // copia [1, 3)

// NO uses delete en arrays (deja hueco)
delete arr[1];

// Vaciar correctamente:
arr.length = 0;           // mas eficiente que arr = []

// Arrays sparse (con huecos)
const sparse = [1, , , 4];
console.log(sparse.length);    // 4
console.log(sparse[1]);        // undefined

🧪 Cómo probarlo

node -e "
const obj = { a: 1, b: 2 };
console.log(Object.keys(obj));        // ['a', 'b']
console.log(Object.values(obj));      // [1, 2]
console.log(Object.entries(obj));     // [['a', 1], ['b', 2]]
"
import assert from "node:assert/strict";
const arr = [1, 2, 3];
assert.strictEqual(arr.length, 3);
arr.push(4);
assert.strictEqual(arr.length, 4);
assert.deepStrictEqual(Object.keys({ a: 1, b: 2 }), ["a", "b"]);

💡 Memoria y rendimiento

OperaciónArrayObject
Acceso por clave/indiceO(1)O(1)
Inserción/borrado finalO(1) amortizadoN/A
Inserción/borrado inicioO(n) reindexaN/A
IteraciónO(n)O(n)
Busqueda de valorO(n) .includes()O(n) Object.values()
  • V8 optimiza arrays como arrays reales (elementos contiguos) si son densos y homogéneos
  • Al añadir tipos mixtos o huecos, V8 los trata como diccionarios lentos
  • ⚠️ delete en arrays deja huecos y desoptimiza
  • Object.freeze() es shallow — las propiedades anidadas siguen siendo mutables

✅ Buenas prácticas

  • Usa Map en lugar de Object para colecciones dinámicas (claves arbitrarias, mejor rendimiento)
  • Prefiere métodos inmutables: .toSorted(), .toReversed(), .toSpliced() (ES2023)
  • ❌ No modifiques el array mientras iteras con forEach/map
  • ❌ No uses for...in en arrays (itera también propiedades heredadas)
  • Para copia superficial: [...arr] o {...obj}. Para profunda: structuredClone(obj)

🏗️ Metodología

  • Objetos: ideales para registros con campos fijos conocidos
  • Arrays: para listas ordenadas del mismo tipo
  • Usa Map para diccionarios que se modifican frecuentemente (mejor rendimiento que Object)
  • En APIs, prefiere objetos planos sobre arrays cuando devuelves datos anidados

🔗 Para saber más

Colecciones: Map, Set, WeakMap, WeakSet

¿Qué es?

Colecciones introducidas en ES6 para cubrir carencias de Object y Array. Map permite claves de cualquier tipo y preserva orden de inserción. Set almacena valores únicos. WeakMap/WeakSet usan referencias débiles que no impiden el garbage collection.

Sintaxis básica

// Map — clave-valor con cualquier tipo de clave
const mapa = new Map();
const objKey = { id: 1 };

mapa.set(objKey, "valor asociado");
mapa.set("string", "otro");
mapa.set(NaN, "NaN como clave");

mapa.get(objKey);   // "valor asociado"
mapa.has("string"); // true
mapa.size;          // 3

for (const [k, v] of mapa) console.log(k, v);

// Set — valores unicos
const conjunto = new Set([1, 2, 3, 2, 1, 3, 4]);
[...conjunto]; // [1, 2, 3, 4]
conjunto.add(5);
conjunto.has(3);     // true (O(1))
conjunto.delete(1);

// Operaciones entre sets
const a = new Set([1, 2, 3]), b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]);                       // {1, 2, 3, 4}
const interseccion = new Set([...a].filter(x => b.has(x))); // {2, 3}
const diferencia = new Set([...a].filter(x => !b.has(x)));  // {1}

// WeakMap — claves objeto, referencias debiles
const cache = new WeakMap();
let comp = { id: 1 };
cache.set(comp, { creado: Date.now() });
comp = null; // WeakMap libera la entrada automaticamente

// WeakSet — valores objeto unicos con referencia debil
const visitados = new WeakSet();
let nodo = { id: 1 };
visitados.add(nodo);
nodo = null; // desaparece del WeakSet

🧪 Cómo probarlo

node -e "
const m = new Map(); m.set('a', 1); m.set('b', 2);
console.log(m.size);            // 2
console.log(m.get('a'));        // 1
console.log(m.has('c'));        // false
const s = new Set([1,2,2,3]);
console.log([...s]);            // [1, 2, 3]
"
import assert from "node:assert/strict";
const m = new Map([["a", 1], ["b", 2]]);
assert.strictEqual(m.size, 2);
assert.strictEqual(m.get("a"), 1);

const s = new Set([1, 2, 2, 3]);
assert.strictEqual(s.size, 3);
assert.ok(s.has(2));

💡 Memoria y rendimiento

ColecciónBúsquedaInserciónBorradoIteración
MapO(1)O(1)O(1)O(n)
SetO(1)O(1)O(1)O(n)
WeakMapO(1)O(1)O(1)No iterable
WeakSetO(1)O(1)O(1)No iterable
Object (como diccionario)O(1)O(1)O(1)O(n)
  • WeakMap/WeakSet no tienen .size, .keys(), .values(), ni son iterables — las referencias débiles pueden desaparecer en cualquier momento
  • Map preserva orden de inserción, Object también (ES2015+), pero Object tiene herencia de prototipo
  • ⚠️ WeakMap evita memory leaks: cuando el objeto clave se recolecta, la entrada desaparece automaticamente

✅ Buenas prácticas

  • Usa Map para diccionarios que se modifican frecuentemente (mejor que Object plano)
  • Usa Set para deduplicación rápida O(n) en lugar de filter/indexOf O(n²)
  • Usa WeakMap para metadatos asociados a objetos DOM o instancias (no impide GC)
  • ❌ No asumas que Object.keys() incluye Symbols (usa Object.getOwnPropertySymbols)
  • ❌ No uses objetos como claves de Map si esperas encontrar la clave por igualdad estructural (compara por referencia)

🏗️ Metodología

  • Colección dinámica con claves arbitrarias → Map
  • Valores únicos con búsqueda rápida → Set
  • Metadatos asociados a objetos (caches) → WeakMap
  • Tracking de objetos visitados → WeakSet
  • Datos planos con claves string conocidas → Object

🔗 Para saber más

TypedArrays y Buffer

¿Qué es?

TypedArrays son vistas de ArrayBuffer que permiten leer/escribir memoria contigua con un tipo fijo. Ideales para datos binarios, WebGL, audio, imágenes. En Node.js, Buffer extiende Uint8Array para I/O de archivos y red.

Sintaxis básica

// ArrayBuffer subyacente
const buffer = new ArrayBuffer(16); // 16 bytes
const vista32 = new Int32Array(buffer);
const vista8 = new Uint8Array(buffer);
vista32[0] = 0x12345678;

// Int32Array
const int32 = new Int32Array(4);
int32[0] = 42;
int32[1] = -7;

// Uint8Array
const bytes = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
new TextDecoder().decode(bytes); // "Hello"

// Float64Array
const floats = new Float64Array(3);
floats[0] = 3.141592653589793;

// Uint8ClampedArray (satura)
const pixeles = new Uint8ClampedArray(4);
pixeles[0] = 300; pixeles[0]; // 255 (saturado)
pixeles[1] = -10; pixeles[1]; // 0   (saturado)

// Tipos disponibles:
// Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
// Int32Array, Uint32Array, Float32Array, Float64Array,
// BigInt64Array, BigUint64Array

// Buffer (Node.js)
import { Buffer } from "buffer";
const buf1 = Buffer.alloc(10);               // 10 bytes a 0
const buf2 = Buffer.from("Hola");
const combined = Buffer.concat([buf2, Buffer.from(" Mundo")]);

🧪 Cómo probarlo

node -e "
const buf = Buffer.from('Hola');
console.log(buf.toString('hex'));              // 486f6c61
console.log(new TextDecoder().decode(new Uint8Array([72,105]))); // Hi
"
import assert from "node:assert/strict";
const buf = Buffer.from("Hola");
assert.strictEqual(buf.toString(), "Hola");
assert.strictEqual(buf[0], 0x48);

💡 Memoria y rendimiento

  • ArrayBuffer es memoria contigua (a diferencia de Array que es un objeto esparcido)
  • TypedArrays son significativamente más rápidos que arrays normales para datos numéricos
  • Buffer.allocUnsafe() asigna memoria sin inicializar (rápido, pero puede exponer datos sensibles)
  • Buffer.alloc() es más seguro (inicializa a 0) pero más lento
  • ⚠️ .slice() en Buffer comparte memoria — modificar el slice afecta al original. Usa .subarray() para copia segura

✅ Buenas prácticas

  • Siempre llenar Buffer.allocUnsafe() con .fill(0) antes de usar (seguridad)
  • Para strings y datos textuales, no necesitas Buffer — usa strings directamente
  • Usa TextEncoder/TextDecoder para conversión string a bytes en lugar de manipulación manual
  • ❌ No asumas endianness — usa DataView para lecturas multi-byte portables

🏗️ Metodología

  • Canvas/WebGL → Uint8ClampedArray (pixeles RGBA)
  • Network I/O → Buffer (Node.js)
  • Audio → Float32Array (muestras de audio)
  • Archivos binarios → Uint8Array + DataView

🔗 Para saber más

Mutabilidad: const no es inmutabilidad, hoisting, TDZ y scope

¿Qué es?

const evita reasignación de la referencia, no congela el contenido del objeto. El hoisting eleva declaraciones al inicio del ámbito. let/const tienen Temporal Dead Zone (TDZ): existen en el ámbito pero son inaccesibles hasta la declaración.

Sintaxis básica

// const no es inmutabilidad
const PI = 3.1416;
// PI = 3; // TypeError

const usuario = { nombre: "Ana" };
usuario.nombre = "Luis";  // ✅ mutación permitida
usuario.email = "l@e.com";// ✅ nuevo campo
// usuario = {};           // TypeError

// Object.freeze (superficial)
const congelado = Object.freeze({ nombre: "Ana", dir: { ciudad: "Madrid" } });
congelado.dir.ciudad = "Barcelona"; // ✅ freeze es shallow

// Hoisting y TDZ
console.log(a); // undefined (var hoisting)
var a = 5;

// console.log(b); // ReferenceError (TDZ)
let b = 10;

// Function declarations: hoisting completo
saludar(); // "Hola"
function saludar() { console.log("Hola"); }

// Function expressions: según la variable
var foo = function () {};

// Scope: global, function y block
const globalVar = "accesible desde cualquier lugar";
function ejemplo() {
  var functionVar = "solo function scope";
  if (true) {
    let blockLet = "solo block scope";
    const blockConst = "tambien block scope";
    var blockVar = "NO tiene block scope";
  }
}

🧪 Cómo probarlo

node -e "
const o = { a: 1 };
o.a = 2; console.log(o.a);          // 2
console.log(typeof undeclaredVar);   // undefined (seguro)
"
import assert from "node:assert/strict";
const obj = { a: 1 };
obj.a = 2;
assert.strictEqual(obj.a, 2);
assert.throws(() => { const x = 1; x = 2; }, TypeError);

💡 Memoria y rendimiento

  • let/const tienen mejor rendimiento que var en bucles (ámbito más reducido)
  • Object.freeze() tiene coste en rendimiento (marca propiedades como no-writable)
  • El hoisting es un comportamiento del parser, no tiene coste en runtime
  • ⚠️ Las closures en bucles con var causan el problema clásico (todas comparten la misma variable)

✅ Buenas prácticas

  • Usa const por defecto, let solo cuando necesites reasignar
  • Nunca uses var en código moderno
  • Si necesitas inmutabilidad real, usa Object.freeze() profundo (librería immer o structuredClone)
  • ❌ No confíes en const para inmutabilidad — es solo no-reasignación
  • ❌ No asumas que hoisting hace accesible la variable antes de su declaración (TDZ)

🏗️ Metodología

  • const para valores que no cambian de referencia (constantes, imports, config)
  • let para contadores de bucle, acumuladores, flags
  • var solo cuando necesitas compatibilidad con navegadores muy antiguos (IE11)

🔗 Para saber más

Garbage Collection

¿Qué es?

V8 (Chrome/Node.js) usa el recolector Orinoco: GC generacional con mark-sweep compactado, concurrente e incremental. Divide objetos en young generation (nursery) y old generation. Los objetos inalcanzables desde la raíz se recolectan automáticamente.

Sintaxis básica

let vivo = { data: "importante" };  // alcanzable → vive
let muerto = { data: "basura" };    // alcanzable
muerto = null;                      // inalcanzable → GC lo limpia

// Ciclos de referencia: V8 los detecta
function test() {
  let a = {}, b = {};
  a.ref = b; b.ref = a;
  // Al salir, a y b son inalcanzables → se recolectan
}

// Forzar GC (solo con --expose-gc):
globalThis.gc?.();

🧪 Cómo probarlo

node --expose-gc -e "
let x = { data: new Array(1000000).fill('a') };
globalThis.gc();          // aún referenciado
x = null;
globalThis.gc();          // ahora se recolecta
console.log('recolectado');
"

💡 Memoria y rendimiento

  • Young generation: objetos nuevos. GC menor (Scavenger) — rápido, pausas < 1ms
  • Old generation: objetos que sobreviven varios GC. GC mayor (Mark-Sweep-Compact) — más lento
  • Pausas típicas: < 1ms (menor), < 10ms (mayor) en V8 moderno
  • ⚠️ Memory leaks comunes:
    • Variables globales accidentales
    • Closures que retienen referencias grandes
    • Timers sin limpiar (setInterval sin clearInterval)
    • Listeners de eventos nunca eliminados
    • Caché sin límite (crece indefinidamente)
    • DOM detached (referencia a elemento eliminado)

✅ Buenas prácticas

  • Usa WeakMap/WeakSet para metadatos asociados a objetos
  • Limpia timers: const id = setInterval(fn, 1000); clearInterval(id);
  • Elimina listeners: emitter.off("evento", callback) o usar once()
  • En closures grandes, asigna null a referencias pesadas tras usarlas
  • ❌ No asumas que delete obj.prop libera memoria (solo elimina la propiedad)

🏗️ Metodología

  • Para apps normales, confía en el GC — no optimices prematuramente
  • Para caches, usa LRU (librería lru-cache) o WeakMap
  • Para streams y archivos grandes, libera buffers explícitamente
  • Monitorea memoria con process.memoryUsage() en Node.js

🔗 Para saber más


Control de flujo y modularidad

Condicionales: if/else, ternario, switch, short-circuit

¿Qué es?

Las estructuras condicionales permiten ejecutar diferentes bloques de código según condiciones booleanas. JavaScript tiene if/else, el operador ternario (cond ? a : b), switch, y short-circuit con &&, ||, ??.

Sintaxis básica

// if/else
const edad = 18;
if (edad < 12) {
  console.log("Niño");
} else if (edad < 18) {
  console.log("Adolescente");
} else if (edad < 65) {
  console.log("Adulto");
} else {
  console.log("Jubilado");
}

// Ternario
const mensaje = edad >= 18 ? "Mayor de edad" : "Menor de edad";

// switch
const dia = new Date().getDay();
switch (dia) {
  case 0: console.log("Domingo"); break;
  case 6: console.log("Sábado"); break;
  case 1: case 2: case 3: case 4: case 5: console.log("Laborable"); break;
  default: console.log("Dia invalido");
}

// Short-circuit
0 && "hola";           // 0
1 || "default";        // 1
null ?? "default";     // "default" (?? solo null/undefined)

🧪 Cómo probarlo

node -e "
const edad = 20;
const msg = edad >= 18 ? 'Adulto' : 'Menor';
console.log(msg);  // Adulto
console.log(0 && 'x');        // 0
console.log(null ?? 'predet'); // predet
"
import assert from "node:assert/strict";
assert.strictEqual(18 >= 18 ? "Adulto" : "Menor", "Adulto");
assert.strictEqual(0 && "x", 0);
assert.strictEqual(null ?? "predet", "predet");

💡 Memoria y rendimiento

  • Ternario y short-circuit no tienen penalización frente a if/else (compilan a lo mismo)
  • switch es O(1) con valores enteros (tabla de saltos), O(n) con strings
  • || y && cortocircuitan: no evalúan el segundo operando si el primero determina el resultado

✅ Buenas prácticas

  • Prefiere ternario para asignaciones simples, if/else para lógica compleja (~3+ ramas)
  • Evita ternarios anidados: ilegibles. Usa if/else o switch
  • Usa ?? para valores por defecto (solo null/undefined), no || (que reacciona a cualquier falsy)
  • ❌ No abuses de short-circuit para control de flujo — puede ser críptico

🏗️ Metodología

  • Condiciones binarias simples → ternario
  • Múltiples condiciones mutuamente excluyentes → switch o if/else if
  • Asignación condicional → ?? o ||
  • Para APIs, siempre valida entrada con condicionales al inicio

🔗 Para saber más

Bucles: for, while, do-while, for-in, for-of

¿Qué es?

JavaScript ofrece múltiples estructuras de bucle. for clásico para contadores, while para condiciones, for...of para iterables, for...in para keys de objetos. Desde ES6, for...of es la opción preferida para arrays.

Sintaxis básica

// for clasico
for (let i = 0; i < 5; i++) console.log(i); // 0 1 2 3 4

// while
let i = 0;
while (i < 5) console.log(i++);

// do-while (se ejecuta al menos una vez)
let j = 0;
do { console.log(j); } while (j > 0); // imprime 0

// for-in: itera sobre KEYS (incluye herencia)
const arr = ["a", "b", "c"];
const obj = { x: 1, y: 2 };
for (const k in arr) console.log(k);   // "0", "1", "2"
for (const k in obj) console.log(k);   // "x", "y"

// for-of: itera sobre VALUES (requiere iterable)
for (const v of arr) console.log(v);   // "a", "b", "c"
for (const c of "Hola") console.log(c); // H, o, l, a
for (const [k, v] of new Map([["a", 1]])) console.log(k, v);

🧪 Cómo probarlo

node -e "
const arr = ['a', 'b', 'c'];
let r = '';
for (const v of arr) r += v;
console.log(r); // abc
"
import assert from "node:assert/strict";
const arr = ["a", "b", "c"];
let r = "";
for (const v of arr) r += v;
assert.strictEqual(r, "abc");

💡 Memoria y rendimiento

  • for clásico es el más rápido (menos overhead de iteración)
  • for...of en arrays tiene overhead mínimo (usa el protocolo iterable)
  • for...in es lento (recorre cadena de prototipos)
  • ⚠️ for...in en arrays es antipatrón: incluye propiedades heredadas y no heredadas

✅ Buenas prácticas

  • Usa for...of para arrays por defecto (legible, seguro)
  • Usa for clásico cuando necesites el índice o control fino
  • Nunca uses for...in en arrays
  • Prefiere map/filter/reduce sobre bucles cuando transformas colecciones

🏗️ Metodología

  • Iterar array → for...of (o .forEach() si no necesitas break)
  • Transformar array → .map(), .filter(), .reduce()
  • Bucle con contador → for (let i = 0; i < n; i++)
  • Iterar keys de objeto → for...in (con hasOwnProperty check)

🔗 Para saber más

Métodos de iteración: forEach, map, filter, reduce

¿Qué es?

JavaScript proporciona métodos funcionales para trabajar con arrays que evitan bucles imperativos. map transforma cada elemento, filter selecciona elementos, reduce acumula, find busca, some/every verifican condiciones.

Sintaxis básica

const nums = [1, 2, 3, 4, 5];

// forEach: ejecuta por cada elemento (sin return)
nums.forEach((n, i) => console.log(`[${i}]: ${n}`));

// map: transforma cada elemento, nuevo array
const dobles = nums.map((n) => n * 2); // [2, 4, 6, 8, 10]

// filter: selecciona los que cumplen
const pares = nums.filter((n) => n % 2 === 0); // [2, 4]

// reduce: acumula
const suma = nums.reduce((acc, n) => acc + n, 0); // 15

// Caso avanzado: agrupar
const agrupado = nums.reduce((acc, n) => {
  const key = n % 2 === 0 ? "pares" : "impares";
  (acc[key] ??= []).push(n);
  return acc;
}, {});

// find: primer elemento que cumple
nums.find((n) => n > 3);     // 4

// some: algun elemento cumple?
nums.some((n) => n > 10);    // false

// every: todos cumplen?
nums.every((n) => n > 0);    // true

🧪 Cómo probarlo

node -e "
const n = [1, 2, 3, 4, 5];
console.log(n.map(x => x * 2));   // [2,4,6,8,10]
console.log(n.filter(x => x%2));  // [1,3,5]
console.log(n.reduce((a,b) => a+b, 0)); // 15
"
import assert from "node:assert/strict";
assert.deepStrictEqual([1, 2, 3].map(x => x * 2), [2, 4, 6]);
assert.deepStrictEqual([1, 2, 3].filter(x => x > 1), [2, 3]);
assert.strictEqual([1, 2, 3].reduce((a, b) => a + b, 0), 6);

💡 Memoria y rendimiento

  • map crea un nuevo array (usa 2x memoria del original)
  • filter crea un nuevo array
  • Encadenar map/filter crea arrays intermedios — para datasets grandes (>100k), considera un solo reduce o un bucle for
  • forEach no tiene return y no se puede encadenar
  • ⚠️ reduce sin valor inicial usa el primer elemento como acumulador (peligro con arrays vacíos)

✅ Buenas prácticas

  • Prefiere map/filter/reduce sobre bucles imperativos (declarativo, sin efectos secundarios)
  • Usa forEach solo para efectos secundarios (logging, I/O)
  • Para rendimiento crítico, un bucle for es 2-5x más rápido que reduce
  • ❌ No modifiques el array original dentro de map/filter — deben ser funciones puras

🏗️ Metodología

  • Transformación 1:1 → map
  • Selección → filter
  • Acumulación → reduce
  • Búsqueda → find (primero) o filter (todos)
  • Verificación → some (algún) o every (todos)
  • Para pipelines complejos, considera flatMap, groupBy, toSorted

🔗 Para saber más

Excepciones: try/catch/finally y throw

¿Qué es?

El manejo de errores en JavaScript usa try/catch/finally. Puedes lanzar errores con throw. Los errores son objetos con name, message y stack. Puedes crear errores personalizados extendiendo Error.

Sintaxis básica

// Básico
try {
  JSON.parse("invalido");
} catch (error) {
  console.error("Error:", error.message);
} finally {
  console.log("Siempre se ejecuta");
}

// Errores personalizados
class ValidationError extends Error {
  constructor(campo, mensaje) {
    super(mensaje);
    this.name = "ValidationError";
    this.campo = campo;
  }
}

class NotFoundError extends Error {
  constructor(recurso, id) {
    super(`${recurso} con id ${id} no encontrado`);
    this.name = "NotFoundError";
    this.statusCode = 404;
  }
}

function buscarUsuario(id) {
  if (id <= 0) throw new ValidationError("id", "Debe ser positivo");
  throw new NotFoundError("Usuario", id);
}

// Manejo por tipo
try {
  buscarUsuario(-1);
} catch (err) {
  if (err instanceof ValidationError) {
    console.error("Validacion:", err.campo, err.message);
  } else if (err instanceof NotFoundError) {
    console.error("No encontrado:", err.message, `(HTTP ${err.statusCode})`);
  } else {
    console.error("Inesperado:", err);
  }
}

🧪 Cómo probarlo

node -e "
try { JSON.parse('invalido'); } catch (e) {
  console.log(e.name, e.message);
}
"
import assert from "node:assert/strict";
assert.throws(() => JSON.parse("invalido"), SyntaxError);
assert.doesNotThrow(() => JSON.parse('"ok"'));

💡 Memoria y rendimiento

  • Lanzar errores es caro (construye stack trace). No uses throw para control de flujo normal
  • try/catch sin error tiene overhead despreciable (~1%)
  • El stack trace se construye en el momento del throw, no del catch
  • ⚠️ Errores dentro de callbacks/promesas no capturados por try/catch externo

✅ Buenas prácticas

  • Siempre lanza objetos Error (o subclases), no strings o números
  • Usa instanceof para distinguir tipos de error
  • En APIs asíncronas, captura errores con try/catch alrededor de await
  • ❌ No captures errores genéricos sin relanzar los que no esperas
  • ❌ No uses throw para control de flujo

🏗️ Metodología

  • Errores de validación → ValidationError (400)
  • Errores de negocio → subclases de Error con statusCode
  • Errores inesperados → captura genérica, log, relanza
  • En APIs Express/Fastify, usa middleware de error centralizado

🔗 Para saber más

Módulos: ESM vs CommonJS, import/export, dynamic import

¿Qué es?

JavaScript tiene dos sistemas de módulos: ESM (ES Modules, estándar moderno con import/export) y CommonJS (CJS, legacy de Node.js con require/module.exports). ESM es estático (análisis en parseo), CJS es dinámico (ejecución).

Sintaxis básica

// ESM
// 📁 math.js
export const PI = 3.1416;
export function sumar(a, b) { return a + b; }
export default class Calc { mul(a, b) { return a * b; } }

// 📁 main.js
import Calc, { PI, sumar } from "./math.js";
import * as Math from "./math.js"; // namespace

// CommonJS
// 📁 math.cjs
const PI = 3.1416;
function sumar(a, b) { return a + b; }
module.exports = { PI, sumar };

// 📁 main.cjs
const { PI, sumar } = require("./math.cjs");

// Dynamic import
const modulo = await import("./ruta/modulo.js");

🧪 Cómo probarlo

echo 'export const hola = "mundo"' > /tmp/test_mod.mjs
node -e "import('/tmp/test_mod.mjs').then(m => console.log(m.hola))"
import assert from "node:assert/strict";
// Test con import dinámico
const mod = await import("path");
assert.ok(mod.join);

Diferencias ESM vs CommonJS

CaracterísticaESMCommonJS
Sintaxisimport/exportrequire()/module.exports
CargaAsincrona (parsea todo antes)Sincrona (ejecuta mientras importa)
AmbitoStrict mode siempreSloppy mode por defecto
this en moduloundefinedmodule.exports
AnalisisEstático (parseo)Dinámico (ejecución)
Top-level await✅❌
Tree shaking✅ automático❌
Extension.mjs o "type": "module".cjs o "type": "commonjs"
Import dinámicoimport() (promesa)require() (sincrono)

💡 Memoria y rendimiento

  • ESM permite tree shaking: bundlers eliminan exports no usados en producción
  • CJS carga módulos sincronamente (bloqueante), ESM es asincrono (no bloqueante)
  • import() dinámico permite code splitting (carga bajo demanda)
  • ⚠️ CJS en Node.js cachea módulos en require.cache

✅ Buenas prácticas

  • Usa ESM para proyectos nuevos (estándar, tree shaking, top-level await)
  • CommonJS solo para herramientas CLI o compatibilidad con módulos legacy
  • Usa extensiones .mjs/.cjs para control explícito del sistema de módulos
  • ❌ No mezcles import y require() en el mismo archivo
  • ❌ No asumas que require() es disponible en ESM (no lo es)

🏗️ Metodología

  • Aplicaciones nuevas → ESM con "type": "module" en package.json
  • Librerías para npm → ESM + CJS (dual package)
  • Scripts simples → .mjs para ESM explícito
  • Herramientas CLI → CJS (más compatible)

🔗 Para saber más


Sistema de archivos (Node.js)

Node.js tiene un potente módulo fs (file system) para trabajar con archivos, path para rutas, stream para flujos, buffer para datos binarios y zlib para compresión.

fs.readFile / fs.writeFile (sincrono y callback)

¿Qué es?

El módulo fs proporciona tres APIs: sincrona (bloqueante), callbacks (no bloqueante) y promesas (fs/promises). La API de promesas con async/await es la recomendada.

Sintaxis básica

import fs from "fs";

// Sincrono (bloqueante)
const data = fs.readFileSync("archivo.txt", "utf8");
fs.writeFileSync("salida.txt", "Contenido del archivo");

// Callback (no bloqueante)
fs.readFile("archivo.txt", "utf8", (err, data) => {
  if (err) { console.error("Error:", err.message); return; }
  console.log(data);
});

fs.writeFile("log.txt", "nueva linea\n", { flag: "a" }); // append

🧪 Cómo probarlo

node -e "
import('fs/promises').then(async fs => {
  await fs.writeFile('/tmp/test.txt', 'Hola mundo');
  const data = await fs.readFile('/tmp/test.txt', 'utf8');
  console.log(data); // Hola mundo
});
"
import assert from "node:assert/strict";
import fs from "fs/promises";
const path = "/tmp/test_assert.txt";
await fs.writeFile(path, "test");
const data = await fs.readFile(path, "utf8");
assert.strictEqual(data, "test");

💡 Memoria y rendimiento

  • readFile carga todo el archivo en memoria — peligro con archivos > 100MB
  • Para archivos grandes (> 100MB), usa streams (createReadStream)
  • La API sincrona bloquea el event loop — nunca en servidores de producción
  • writeFile con { flag: "a" } abre, escribe y cierra cada vez — para logs frecuentes usa streams

✅ Buenas prácticas

  • Usa fs/promises con async/await (moderno, legible, manejo de errores)
  • Usa streams para archivos grandes, readFile para pequeños (< 50MB)
  • Siempre usa try/catch con la API de promesas
  • ❌ No uses readFileSync en servidores web (bloquea el hilo)

🏗️ Metodología

  • Archivos de configuración (< 1MB) → readFile (promesas)
  • Logs en tiempo real → createWriteStream
  • Archivos grandes (> 100MB) → streams con pipe
  • Procesamiento por lotes → readFile + writeFile en workers

🔗 Para saber más

path module y rutas

¿Qué es?

path proporciona utilidades para trabajar con rutas de archivos: unir, resolver, extraer componentes, convertir rutas relativas a absolutas. Es multiplataforma (maneja separadores Windows vs POSIX).

Sintaxis básica

import path from "path";

path.basename("/usr/local/bin/node");     // "node"
path.dirname("/usr/local/bin/node");      // "/usr/local/bin"
path.extname("foto.jpg");                 // ".jpg"

path.join("/usr", "local", "bin");       // "/usr/local/bin"
path.resolve("src", "..", "dist");       // ruta absoluta desde cwd

path.parse("/home/user/file.txt");
// { root: "/", dir: "/home/user", base: "file.txt", ext: ".txt", name: "file" }

// En ESM (no hay __dirname):
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

🧪 Cómo probarlo

node -e "
import('path').then(p => {
  console.log(p.basename('/a/b/c.js'));  // c.js
  console.log(p.extname('/a/b/c.js'));   // .js
});
"

💡 Memoria y rendimiento

  • path.join y path.resolve son operaciones O(n) sobre la longitud de la ruta
  • No hay penalización significativa — se usan una vez al inicio típicamente
  • ⚠️ path.resolve sin argumentos devuelve el directorio actual (process.cwd())

✅ Buenas prácticas

  • Usa siempre path.join o path.resolve en lugar de concatenar strings con /
  • Nunca asumas el separador (/ en POSIX, \ en Windows) — path.sep lo maneja
  • Para rutas URL (web), usa URL y URLSearchParams, no path

🏗️ Metodología

  • Rutas absolutas → path.resolve
  • Rutas relativas → path.join
  • Separación de componentes → path.parse, path.basename, path.dirname
  • Rutas relativas entre dos directorios → path.relative

🔗 Para saber más

Streams (pipe, transform)

¿Qué es?

Los streams permiten procesar datos por fragmentos (chunks) sin cargar todo en memoria. Hay 4 tipos: readable, writable, transform, duplex. Ideales para archivos grandes, network I/O, pipelines de transformación.

Sintaxis básica

import { createReadStream, createWriteStream } from "fs";
import { Transform, pipeline } from "stream";

// Transform: modifica datos en transito
const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

// Pipe: readable → transform → writable
createReadStream("input.txt")
  .pipe(upperCase)
  .pipe(createWriteStream("output.txt"))
  .on("finish", () => console.log("Completado"));

// pipeline (con promesa, manejo de errores automático):
import { pipeline } from "stream/promises";
await pipeline(
  createReadStream("input.txt"),
  upperCase,
  createWriteStream("output.txt"),
);

// Leer por chunks
const rs = createReadStream("archivo.txt", {
  encoding: "utf8",
  highWaterMark: 64 * 1024, // 64KB por chunk
});
rs.on("data", (chunk) => console.log("Chunk:", chunk.length, "bytes"));

🧪 Cómo probarlo

echo "Hola mundo" > /tmp/test_stream.txt
node -e "
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
await pipeline(
  createReadStream('/tmp/test_stream.txt'),
  createWriteStream('/tmp/test_copia.txt')
);
console.log('OK');
"
import assert from "node:assert/strict";
import { Readable } from "stream";
const chunks = [];
const rs = new Readable({
  read() { this.push("data"); this.push(null); }
});
rs.on("data", c => chunks.push(c));
await new Promise(r => rs.on("end", r));
assert.strictEqual(Buffer.concat(chunks).toString(), "data");

💡 Memoria y rendimiento

  • Los streams usan memoria proporcional al chunk size (default 64KB), no al archivo completo
  • highWaterMark controla el buffering interno (default 16KB para readable, 16KB para writable)
  • Backpressure: el stream readable frena automáticamente si el writable está saturado
  • ⚠️ Si no consumes el stream (sin data listener o pipe), se bufferiza en memoria

✅ Buenas prácticas

  • Usa pipeline en lugar de .pipe() (maneja backpressure y errores automáticamente)
  • Para transformaciones, usa Transform en lugar de readable + writable separados
  • Siempre maneja eventos error en streams
  • ❌ No conviertas streams enteros a buffer con métodos manuales a menos que sea necesario

🏗️ Metodología

  • Archivos grandes (> 100MB) → streams
  • Compresión en vuelo → zlib + pipeline
  • Procesamiento ETL → Transform streams encadenados
  • Respuestas HTTP → pipe del stream directamente a res (Response)

🔗 Para saber más

Buffer y zlib

¿Qué es?

Buffer (Node.js) es una subclase de Uint8Array para trabajar con datos binarios. zlib proporciona compresión/descompresión gzip, brotli, deflate. Ambos son esenciales para I/O de red, archivos binarios y HTTP.

Sintaxis básica

import { Buffer } from "buffer";

// Crear buffers
const buf1 = Buffer.alloc(10);               // 10 bytes, inicializados a 0
const buf2 = Buffer.alloc(10, 0xff);         // 10 bytes, todos 0xff
const buf3 = Buffer.from("Hola");
const buf4 = Buffer.from([0x48, 0x6f, 0x6c, 0x61]);
const combined = Buffer.concat([buf3, Buffer.from(" Mundo")]);

// zlib
import { createGzip, createGunzip } from "zlib";
import { pipeline } from "stream/promises";

await pipeline(
  createReadStream("datos.json"),
  createGzip(),
  createWriteStream("datos.json.gz"),
);

// Comprimir en memoria
import { gzip, gunzip } from "zlib";
const data = "texto repetido ".repeat(1000);
const compressed = await new Promise((resolve, reject) => {
  gzip(data, (err, result) => err ? reject(err) : resolve(result));
});

✅ Buenas prácticas

  • Usa Buffer.allocUnsafe() solo cuando sepas que llenarás el buffer inmediatamente
  • Para compresión HTTP, usa zlib con pipeline para streaming
  • ❌ No asumas que Buffer está disponible en navegadores (usa TextEncoder/TextDecoder)

🔗 Para saber más

readdir y watch

¿Qué es?

fs.readdir lista el contenido de un directorio. fs.watch observa cambios en archivos o directorios. Útiles para herramientas de desarrollo, recarga en caliente, procesamiento por lotes.

Sintaxis básica

import fs from "fs/promises";

// Listar archivos
const archivos = await fs.readdir("./");
const entries = await fs.readdir("./", { withFileTypes: true });
for (const entry of entries) {
  if (entry.isFile()) {
    const stats = await fs.stat(entry.name);
    console.log(`${entry.name} (${stats.size} bytes)`);
  }
}

// Observar cambios
import fs from "fs";
const watcher = fs.watch("./src", { recursive: true }, (eventType, filename) => {
  console.log(`${eventType}: ${filename}`);
});

🔗 Para saber más


Algoritmos y estructuras de datos

Array.sort y Timsort

¿Qué es?

Desde V8 7.0, Array.prototype.sort usa Timsort (estable, O(n log n)). Por defecto ordena como strings (convierte elementos a string y compara lexicográficamente). Para ordenación numérica, debes pasar un comparador.

Sintaxis básica

const nums = [3, 1, 4, 1, 5, 9, 2, 6];

// Ordenación por defecto: alfabética
nums.sort();          // [1, 1, 2, 3, 4, 5, 6, 9]

// Peligro:
[10, 2, 1].sort();    // [1, 10, 2] — compara strings: "10" < "2"

// Ordenación numérica con comparador:
nums.sort((a, b) => a - b);  // ascendente
nums.sort((a, b) => b - a);  // descendente

// Timsort es ESTABLE
const estudiantes = [
  { nombre: "Ana", nota: 85 },
  { nombre: "Luis", nota: 92 },
  { nombre: "Sofia", nota: 85 },
];
estudiantes.sort((a, b) => a.nota - b.nota); // Ana antes que Sofia (estable)

// toSorted (ES2023) — copia, no muta
const copia = nums.toSorted((a, b) => a - b);

🧪 Cómo probarlo

node -e "
const n = [3, 1, 4, 1, 5];
console.log(n.toSorted((a,b) => a-b)); // [1, 1, 3, 4, 5]
console.log(n);                         // [3, 1, 4, 1, 5] (original intacto)
"
import assert from "node:assert/strict";
assert.deepStrictEqual([3, 1, 4].toSorted(), [1, 3, 4]);
assert.deepStrictEqual([3, 1, 4].toSorted((a, b) => b - a), [4, 3, 1]);

💡 Memoria y rendimiento

  • Timsort: O(n log n) peor caso, O(n) en arrays casi ordenados
  • sort() muta el array original — usa toSorted() para inmutabilidad (ES2023)
  • Comparador debe ser transitivo: si a > b y b > c, entonces a > c
  • ⚠️ Timsort en V8 usa memoria extra O(n) en el peor caso

✅ Buenas prácticas

  • Siempre pasa un comparador para números: (a, b) => a - b
  • Para strings, usa localeCompare: (a, b) => a.localeCompare(b)
  • Usa toSorted() en lugar de sort() cuando no quieras mutar el original
  • ❌ No asumas que sort() sin argumentos ordena números correctamente

🏗️ Metodología

  • Orden simple de números → sort((a,b) => a-b)
  • Orden multi-criterio → comparador compuesto
  • Objetos con strings → localeCompare
  • Rendimiento crítico → considerar Intl.Collator para strings

🔗 Para saber más

Búsqueda y Big O de colecciones

¿Qué es?

La búsqueda en colecciones varía drásticamente: Set.has() y Map.get() son O(1) por hash; Array.includes() y Array.find() son O(n) lineal. Elegir la estructura correcta es clave para el rendimiento.

Sintaxis básica

// Set.has() es O(1)
const set = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
console.log(set.has(7));  // true, O(1)

// Array.includes() es O(n)
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(array.includes(7));  // true, O(n)

// Map.get() es O(1)
const map = new Map(array.map((n) => [n, n]));
console.log(map.get(7));  // 7, O(1)

// Binary search manual (array ordenado)
function binarySearch(arr, target) {
  let left = 0, right = arr.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

// find, findIndex, findLastIndex
sorted.find((n) => n > 5);              // 7
sorted.findIndex((n) => n > 5);         // 3
sorted.findLastIndex((n) => n < 10);    // 4 (ES2023)

// Object.groupBy (ES2024)
const inventario = [
  { nombre: "manzana", categoria: "fruta" },
  { nombre: "zanahoria", categoria: "verdura" },
];
const porCategoria = Object.groupBy(inventario, (item) => item.categoria);

🧪 Cómo probarlo

node -e "
const s = new Set([1,2,3]);
console.log(s.has(2)); // true
console.log(s.has(5)); // false
"
import assert from "node:assert/strict";
const s = new Set([1, 2, 3]);
assert.ok(s.has(2));
assert.strictEqual(s.has(5), false);
const m = new Map([["a", 1]]);
assert.strictEqual(m.get("a"), 1);

💡 Memoria y rendimiento (Big O)

OperaciónArraySetMapTypedArray
Acceso por indiceO(1)N/AN/AO(1)
Búsqueda .includes() / .has()O(n)O(1)O(1)O(n)
Inserción al final .push()O(1) amortizadoO(1)O(1)O(1)
Inserción al inicio .unshift()O(n)N/AN/AO(n)
Eliminación por valorO(n)O(1)O(1)O(n)
IteraciónO(n)O(n)O(n)O(n)
Ordenación .sort()O(n log n)N/AN/AO(n log n)

✅ Buenas prácticas

  • Búsqueda frecuente → Set (valores) o Map (clave-valor), O(1)
  • Necesitas orden → Array (Set/Map preservan inserción pero no se reordenan)
  • Filtrado eficiente: convierte a Set primero
  • ❌ No uses filter + includes para exclusión (O(n²)) — usa Set

🏗️ Metodología

  • Deduplicación → new Set(array)
  • Búsqueda rápida con datos planos → Set.has()
  • Diccionario con claves arbitrarias → Map
  • Ordenación de objetos → sort con comparador

🔗 Para saber más


Conceptos clave explicados a fondo

Promesas, async/await, event loop y microtasks

¿Qué es?

Una promesa representa un valor futuro. async/await es azúcar sintáctico sobre promesas. El event loop es el corazón de la concurrencia en JS: gestiona la ejecución de tareas sincronas, microtasks (promesas) y macrotasks (timers, I/O). JS es single-threaded pero no bloqueante gracias al event loop.

Sintaxis básica

// Promesa
const promesa = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Datos cargados"), 1000);
});
promesa.then(console.log).catch(console.error).finally(() => console.log("fin"));

// async/await
async function obtenerUsuario(username) {
  const res = await fetch(`https://api.github.com/users/${username}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return await res.json();
}

// Metodos estaticos
Promise.all([p1, p2, p3]);          // todas (falla rapido)
Promise.allSettled([p1, p2]);       // todas (nunca falla)
Promise.race([p1, p2]);             // primera en resolverse
Promise.any([Promise.reject("x"), p2]); // primera exitosa

// Event Loop - microtasks vs macrotasks
console.log("1 sincrono");
setTimeout(() => console.log("2 macrotask"), 0);
Promise.resolve().then(() => console.log("3 microtask"));
console.log("4 sincrono");
// Output: 1, 4, 3, 2

🧪 Cómo probarlo

node -e "
Promise.resolve(42).then(console.log); // 42
"
import assert from "node:assert/strict";
const p = Promise.resolve(42);
assert.strictEqual(await p, 42);

// Event loop order
const order = [];
order.push(1);
setTimeout(() => order.push(3), 0);
Promise.resolve().then(() => order.push(2));
await new Promise(r => setTimeout(r, 10));
assert.deepStrictEqual(order, [1, 2, 3]);

💡 Memoria y rendimiento

  • Las promesas tienen overhead frente a callbacks (~2-5x más lentas en creación)
  • Pero son mucho más manejables que callback hell
  • Promise.all ejecuta en paralelo (concurrencia), no secuencial
  • Microtasks tienen prioridad sobre macrotasks — las promesas se ejecutan antes que setTimeout
  • ⚠️ Cadenas de promesas muy largas pueden consumir memoria (cada .then() crea una nueva promesa)

✅ Buenas prácticas

  • Usa async/await en lugar de .then()/.catch() (más legible)
  • Siempre maneja errores con try/catch alrededor de await
  • Usa Promise.all para operaciones independientes, no secuenciales
  • Evita Promise.all con arrays gigantes (crea todas las promesas simultáneamente)
  • ❌ No mezcles callbacks y promesas en el mismo flujo
  • ❌ No olvides await dentro de async functions (la promesa no se resolverá)

🏗️ Metodología

  • I/O (HTTP, BD, archivos) → async/await
  • Operaciones paralelas independientes → Promise.all
  • Operaciones con timeout → Promise.race + AbortController
  • Flujos de datos continuos → Async iterables (for await...of)
  • Para rate limiting → librería p-limit

🔗 Para saber más

Closures y scope

¿Qué es?

Un closure es una función que recuerda el ámbito donde fue creada, incluso después de que ese ámbito haya terminado. Es el mecanismo fundamental para encapsulación en JS. El scope determina qué variables son accesibles: global, function (con var) y block (con let/const).

Sintaxis básica

// Closure
function crearContador(inicial = 0) {
  let cuenta = inicial;  // Variable "privada"
  return {
    incrementar() { return ++cuenta; },
    obtener() { return cuenta; },
  };
}
const contador = crearContador(10);
console.log(contador.incrementar()); // 11

// El problema del bucle con var
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3
}
// Solucion: let (block scope)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// IIFE (Immediately Invoked Function Expression)
const modulo = (function () {
  const privado = "solo aqui dentro";
  return { publico: () => privado.toUpperCase() };
})();

// Debounce con closure
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

🧪 Cómo probarlo

node -e "
function crearC() { let c = 0; return () => ++c; }
const c = crearC();
console.log(c()); // 1
console.log(c()); // 2
"
import assert from "node:assert/strict";
function crearC() { let c = 0; return () => ++c; }
const c = crearC();
assert.strictEqual(c(), 1);
assert.strictEqual(c(), 2);

💡 Memoria y rendimiento

  • Cada closure mantiene viva la variable capturada en memoria (no se recolecta mientras exista la función)
  • Closures en bucles con var comparten la misma variable (todas ven el valor final)
  • Las closures tienen overhead mínimo (~bytes adicionales por variable capturada)
  • ⚠️ Closures grandes que capturan objetos pesados pueden causar memory leaks

✅ Buenas prácticas

  • Usa closures para encapsular estado privado (patrón módulo)
  • Usa let en bucles para crear closures independientes
  • Para closures que capturan objetos grandes, asigna null cuando ya no se necesiten
  • ❌ No crees funciones dentro de bucles a menos que sea necesario (cada iteración crea un nuevo closure)

🏗️ Metodología

  • Encapsulación de estado → patrón módulo (closure)
  • Middleware/pipelines → closures capturan configuración
  • Event handlers → closures preservan estado del componente
  • Currying/partial application → closures capturan argumentos parciales
  • React Hooks → useState, useEffect usan closures para preservar estado entre renders

🔗 Para saber más

this, bind, call, apply

¿Qué es?

this se determina en tiempo de ejecución según cómo se llama la función, no dónde se define. Las arrow functions no tienen su propio this — heredan del ámbito léxico. bind/call/apply permiten controlar this explícitamente.

Sintaxis básica

// 1. Llamada normal: this → global (o undefined en strict mode)
function mostrar() { "use strict"; console.log(this); // undefined }

// 2. Metodo de objeto: this → el objeto
const obj = { nombre: "obj", mostrar };
obj.mostrar(); // { nombre: "obj", mostrar: f }

// 3. Constructor (new): this → la nueva instancia
new Persona("Ana"); // this → instancia nueva

// 4. Arrow function: hereda this del ambito lexico
const objeto = {
  nombre: "objeto",
  correcto: function () {
    setTimeout(() => console.log(this.nombre), 100); // "objeto"
  },
};

// bind, call, apply
function saludar(prefix, suffix) {
  return `${prefix} ${this.nombre}${suffix}`;
}
const user = { nombre: "Ana" };
saludar.call(user, "Hola", "!");   // "Hola Ana!"
saludar.apply(user, ["Hola", "!"]); // "Hola Ana!"
const saludarAna = saludar.bind(user, "Hola");
saludarAna("!");                    // "Hola Ana!"

🧪 Cómo probarlo

node -e "
const o = { n: 42, f() { return this.n; } };
console.log(o.f()); // 42
"
import assert from "node:assert/strict";
const o = { n: 42, f() { return this.n; } };
assert.strictEqual(o.f(), 42);

💡 Memoria y rendimiento

  • bind() crea una nueva función (coste de asignación)
  • call/apply son equivalentes en rendimiento; usa call si conoces los argumentos
  • Arrow functions no tienen arguments ni new.target
  • ⚠️ Perder this es común al pasar métodos como callbacks: const f = obj.metodo; f() pierde this

✅ Buenas prácticas

  • Usa arrow functions para evitar perder this (callbacks, event handlers)
  • Usa bind para fijar this permanentemente en un método
  • Prefiere call sobre apply a menos que los argumentos sean dinámicos
  • ❌ No uses arrow functions para métodos de objetos (pierden this dinámico)
  • ❌ No crees funciones.bind() dentro de renders (React) o bucles — crea una nueva cada vez

🏗️ Metodología

  • Métodos de clase → funciones regulares (necesitan this dinámico)
  • Callbacks y closures → arrow functions (heredan this)
  • APIs que requieren this explícito → bind la primera vez, luego reusa
  • En React class components: .bind(this) en constructor o arrow functions en class fields

🔗 Para saber más

Proxy, Reflect, y metaprogramación

¿Qué es?

Proxy permite interceptar operaciones sobre un objeto (get, set, has, delete, etc.). Reflect proporciona métodos para realizar esas operaciones de forma estándar. Juntos permiten metaprogramación: validación, logging, observabilidad, virtualización.

Sintaxis básica

const objetivo = { mensaje: "hola" };

const proxy = new Proxy(objetivo, {
  get(target, prop, receiver) {
    if (prop in target) return Reflect.get(target, prop, receiver);
    return `Propiedad "${String(prop)}" no existe`;
  },
  set(target, prop, value) {
    if (prop === "edad" && typeof value !== "number") {
      throw new TypeError("edad debe ser un numero");
    }
    return Reflect.set(target, prop, value);
  },
  has(target, prop) {
    console.log(`Verificando "${String(prop)}"`);
    return prop in target;
  },
  deleteProperty(target, prop) {
    console.log(`Eliminando "${String(prop)}"`);
    return delete target[prop];
  },
});

console.log(proxy.mensaje);    // "hola"
console.log(proxy.noExiste);   // "Propiedad 'noExiste' no existe"
proxy.edad = 30;               // OK

🧪 Cómo probarlo

node -e "
const p = new Proxy({}, { get(t, prop) { return prop in t ? t[prop] : 'default'; } });
console.log(p.x); // default
p.x = 42;
console.log(p.x); // 42
"
import assert from "node:assert/strict";
const p = new Proxy({}, { get(t, prop) { return prop in t ? t[prop] : 42; } });
assert.strictEqual(p.anything, 42);

💡 Memoria y rendimiento

  • Proxy tiene overhead significativo (cada operación pasa por los traps)
  • No uses Proxy en hot paths (bucles estrechos, renders frecuentes)
  • Reflect tiene rendimiento similar a las operaciones directas
  • ⚠️ Proxy no es transpilable a ES5 — cuidado con compatibilidad

✅ Buenas prácticas

  • Usa Proxy para validación de esquemas, logging, acceso remoto (virtual proxy)
  • Usa Reflect para implementar traps de Proxy (siempre usa Reflect.get/set dentro de traps)
  • ❌ No abuses de Proxy para getters/setters simples (classes hacen lo mismo sin overhead)
  • ❌ Proxy no reemplaza Object.defineProperty para propiedades específicas

🏗️ Metodología

  • Validación de esquemas → Proxy en set trap
  • Logging automático → Proxy en get/set traps
  • Objetos virtuales (API remota) → Proxy con get que hace fetch
  • Observabilidad (Vue 3, MobX) → Proxy detecta cambios en objetos

🔗 Para saber más


POO y patrones de diseño

Clases y prototipos

¿Qué es?

JavaScript implementa POO mediante prototipos (no clases clásicas). Las clases ES6+ son azúcar sintáctico sobre prototipos. Cada objeto tiene un [[Prototype]] (otro objeto del que hereda propiedades). La cadena termina en Object.prototype → null.

Sintaxis básica

// Prototipos
function Animal(nombre) { this.nombre = nombre; }
Animal.prototype.saludar = function () {
  return `Hola, soy ${this.nombre}`;
};
const perro = new Animal("Rex");
console.log(perro.saludar()); // "Hola, soy Rex"

// Clases (mismo mecanismo interno)
class Persona {
  #edad = 0;  // Campo privado (ES2022)
  constructor(nombre) { this.nombre = nombre; }
  saludar() { return `Hola, soy ${this.nombre}`; }
  get edad() { return this.#edad; }
  set edad(valor) {
    if (typeof valor !== "number") throw new TypeError("edad debe ser numero");
    this.#edad = valor;
  }
  static crear(nombre) { return new Persona(nombre); }
}

console.log(typeof Persona); // "function"
console.log(Persona.prototype.saludar); // [Function: saludar]

// Herencia
class Empleado extends Persona {
  constructor(nombre, puesto) {
    super(nombre);
    this.puesto = puesto;
  }
  saludar() { return `${super.saludar()} y soy ${this.puesto}`; }
}

🧪 Cómo probarlo

node -e "
class A { constructor(v) { this.v = v; } getV() { return this.v; } }
const a = new A(42);
console.log(a.getV()); // 42
"
import assert from "node:assert/strict";
class A { constructor(v) { this.v = v; } getV() { return this.v; } }
assert.strictEqual(new A(42).getV(), 42);

💡 Memoria y rendimiento

  • Los métodos en el prototipo se comparten entre todas las instancias (no se copian)
  • Campos privados # tienen rendimiento similar a propiedades normales
  • instanceof recorre la cadena de prototipos (O(n) en profundidad de herencia)
  • ⚠️ No abuses de herencia profunda (> 3 niveles) — perjudica rendimiento y legibilidad

✅ Buenas prácticas

  • Prefiere composición sobre herencia (mixins, composición de objetos)
  • Usa clases para modelar entidades con estado y comportamiento
  • Marca campos privados con # para encapsulación real (no solo _ por convención)
  • ❌ No creas jerarquías de herencia profundas (> 2-3 niveles)
  • ❌ No confundas instanceof con type checking (falla entre realms/frames)

🏗️ Metodología

  • Entidades del dominio → clases con campos privados y getters/setters
  • Comportamiento compartido → composición (mixins o funciones)
  • Jerarquías naturales → herencia (Animal → Perro, pero no Forma → Cuadrado → Tablero)
  • En React/Vue/Svelte, prefiere hooks/composables sobre clases

🔗 Para saber más

Ejemplo completo POO

// Sistema multimedia con POO completo
const FormatoVideo = Object.freeze({ MP4: "mp4", WEBM: "webm", AVI: "avi" });

class Contenido {
  #reproducciones = 0;
  constructor(titulo, duracionSegundos, tags = []) {
    this.titulo = titulo;
    this.duracionSegundos = duracionSegundos;
    this.tags = tags;
  }
  get reproducciones() { return this.#reproducciones; }
  get duracionFormateada() {
    const min = Math.floor(this.duracionSegundos / 60);
    const seg = this.duracionSegundos % 60;
    return `${min}:${String(seg).padStart(2, "0")}`;
  }
  reproducir() { this.#reproducciones++; return `Reproduciendo ${this.titulo}`; }
  obtenerInfo() { throw new Error("Metodo abstracto"); }
}

class Audio extends Contenido {
  constructor(titulo, duracionSegundos, bitrateKbps, tags = []) {
    super(titulo, duracionSegundos, tags);
    this.bitrateKbps = bitrateKbps;
  }
  obtenerInfo() {
    return `Audio: ${this.titulo} | ${this.duracionFormateada} | ${this.bitrateKbps}kbps`;
  }
}

class Video extends Contenido {
  #subtitulos = false;
  constructor(titulo, duracionSegundos, resolucion, formato, tags = []) {
    super(titulo, duracionSegundos, tags);
    this.resolucion = resolucion;
    this.formato = formato;
  }
  get subtitulos() { return this.#subtitulos; }
  set subtitulos(valor) {
    if (typeof valor !== "boolean") throw new TypeError("subtitulos debe ser booleano");
    this.#subtitulos = valor;
  }
  obtenerInfo() {
    return `Video: ${this.titulo} | ${this.resolucion} | ${this.formato}`;
  }
}

// Uso
const cancion = new Audio("Bohemian Rhapsody", 354, 320, ["rock"]);
const pelicula = new Video("Inception", 8880, "4K", FormatoVideo.MKV);
pelicula.subtitulos = true;

for (const c of [cancion, pelicula]) {
  console.log(c.reproducir());
  console.log(c.obtenerInfo());
}

Patrones de diseño

Module Pattern (IIFE + closure)

const RepositorioUsuarios = (function () {
  const usuarios = new Map();
  let ultimoId = 0;
  return {
    crear(nombre, email) {
      const id = ++ultimoId;
      usuarios.set(id, { id, nombre, email });
      return id;
    },
    obtener(id) { return usuarios.get(id); },
    listar() { return [...usuarios.values()]; },
    get total() { return usuarios.size; },
  };
})();

Observer (EventEmitter)

class EventEmitter {
  #listeners = new Map();
  on(evento, callback) {
    if (!this.#listeners.has(evento)) this.#listeners.set(evento, []);
    this.#listeners.get(evento).push(callback);
    return () => this.off(evento, callback);
  }
  off(evento, callback) {
    const cbs = this.#listeners.get(evento);
    if (cbs) { const idx = cbs.indexOf(callback); if (idx >= 0) cbs.splice(idx, 1); }
  }
  emit(evento, ...args) {
    const cbs = this.#listeners.get(evento);
    if (cbs) cbs.forEach((cb) => cb(...args));
  }
}

Singleton con ESM

// config.js
export const config = new Config(); // ESM: siempre la misma instancia

Factory (registro dinámico)

class Plugin {
  static #registry = new Map();
  static registrar(nombre, clase) { this.#registry.set(nombre, clase); }
  static crear(tipo, ...args) {
    const Clase = this.#registry.get(tipo);
    if (!Clase) throw new Error(`Plugin '${tipo}' no existe`);
    return new Clase(...args);
  }
}

Strategy

const ordenarAsc = (datos) => [...datos].sort((a, b) => a - b);
const ordenarDesc = (datos) => [...datos].sort((a, b) => b - a);
class Analizador {
  constructor(estrategia) { this.estrategia = estrategia; }
  analizar(datos) { return this.estrategia(datos); }
}
PatronEn JavaScript
ModuleIIFE + closure, o ESM nativo
ObserverEventEmitter, addEventListener
SingletonESM module (singleton natural), o clase con #instancia
FactoryRegistro dinámico con Map
StrategyFunciones como ciudadanos de primera clase
Chain of Resp.Middleware (Express, Koa)
AdapterDuck typing

🔗 Para saber más


Polimorfismo en detalle

JavaScript tiene polimorfismo estructural (duck typing) como mecanismo principal, pero también admite polimorfismo por herencia y por interfaz implícita.

1. Duck typing (el mecanismo principal de JS)

class Perro { sonido() { return "Guau"; } }
class Gato { sonido() { return "Miau"; } }
class Coche { sonido() { return "Brum"; } }

function hacerSonido(cualquierCosa) {
  console.log(cualquierCosa.sonido());
}
hacerSonido(new Perro());  // Guau
hacerSonido(new Gato());   // Miau
hacerSonido(new Coche());  // Brum

2. Polimorfismo por herencia

class Forma { area() { throw new Error("Abstracto"); } }
class Circulo extends Forma {
  constructor(radio) { super(); this.radio = radio; }
  area() { return Math.PI * this.radio ** 2; }
}
class Rectangulo extends Forma {
  constructor(ancho, alto) { super(); this.ancho = ancho; this.alto = alto; }
  area() { return this.ancho * this.alto; }
}

3. Polimorfismo paramétrico (TypeScript)

interface Imprimible { imprimir(): string; }
class Documento implements Imprimible { imprimir() { return "Documento"; } }
function enviarAImpresion(objeto: Imprimible) { console.log(objeto.imprimir()); }

En JS puro, las interfaces no existen en runtime — todo es duck typing. TypeScript compila a JS y las interfaces desaparecen. Es duck typing con documentación estática.

🔗 Para saber más


Interacción con contenido multimedia

JavaScript tiene APIs nativas en el navegador para gráficos 2D, audio y video, y potentes librerías en Node.js para procesamiento de imágenes y video.

Canvas API (2D drawing)

const canvas = document.getElementById("miCanvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 100, 50);
ctx.beginPath();
ctx.arc(200, 200, 50, 0, Math.PI * 2);
ctx.fill();
ctx.fillText("Hola Canvas", 10, 200, "24px Arial");

// Animacion
let x = 0;
function animar() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillRect(x, 100, 50, 50);
  x = (x + 2) % canvas.width;
  requestAnimationFrame(animar);
}

ImageData (procesamiento de pixeles)

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray [R,G,B,A,R,G,B,A,...]

// Escala de grises
for (let i = 0; i < data.length; i += 4) {
  const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
  data[i] = data[i+1] = data[i+2] = gray;
}
ctx.putImageData(imageData, 0, 0);

Web Audio API

const audioCtx = new AudioContext();
const oscillator = audioCtx.createOscillator();
oscillator.type = "sine";
oscillator.frequency.value = 440; // La4
const gainNode = audioCtx.createGain();
gainNode.gain.value = 0.5;
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start();

Sharp (Node.js — procesamiento de imágenes)

npm install sharp
import sharp from "sharp";
await sharp("input.jpg").resize(300, 200).toFile("output.jpg");
await sharp("input.png").webp({ quality: 80 }).toFile("output.webp");
const metadata = await sharp("foto.jpg").metadata();
TipoLibreriaFormatosAsync
ImagenesSharpJPEG, PNG, WebP, AVIF, GIF✅
ImagenesJimpJPEG, PNG, BMP, TIFF✅
Videofluent-ffmpegMP4, AVI, MOV, MKV✅
GIFgifencoderGIF✅
CanvasNativo (navegador)PNG, JPEGN/A

🔗 Para saber más


Bases de datos

ORMs para Node.js

LibreriaAsyncTypeScriptPara que
Prisma✅✅ORM moderno, schema declarativo, migrations
Drizzle ORM✅✅SQL-like, tipado fuerte
Mongoose✅✅ODM para MongoDB
Knex.js✅✅Query builder SQL

Prisma (ejemplo completo)

// prisma/schema.prisma
model Usuario {
  id   Int    @id @default(autoincrement())
  email String @unique
  nombre String
  posts Post[]
}
model Post {
  id        Int     @id @default(autoincrement())
  titulo    String
  contenido String?
  autor     Usuario @relation(fields: [autorId], references: [id])
  autorId   Int
}
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const usuario = await prisma.usuario.create({
  data: { email: "ana@email.com", nombre: "Ana" },
});
const posts = await prisma.post.findMany({
  where: { publicado: true },
  include: { autor: true },
  orderBy: { creadoEn: "desc" },
  take: 10,
});

🔗 Para saber más


WebSockets y mensajería

WebSocket nativo

// server.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => {
  ws.on("message", (data) => {
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) client.send(`Echo: ${data}`);
    });
  });
  ws.send("Bienvenido");
});

Socket.IO (con salas)

import { Server } from "socket.io";
const io = new Server(3000, { cors: { origin: "*" } });
io.on("connection", (socket) => {
  socket.on("unirse-sala", (sala) => socket.join(sala));
  socket.on("mensaje-sala", ({ sala, mensaje }) => io.to(sala).emit("mensaje", mensaje));
});
SituacionRecomendación
Aplicación simple, Node 21+WebSocket API nativa
Salas, eventos nombrados, fallbackSocket.IO
Microservicios con streamingws directamente

🔗 Para saber más


Concurrencia y paralelismo

JavaScript es single-threaded pero ofrece múltiples modelos de concurrencia.

Event Loop (concurrencia cooperativa)

Para I/O-bound (HTTP, BD, archivos), el event loop es suficiente:

async function descargarMuchos(urls) {
  return Promise.all(urls.map((url) => fetch(url).then((r) => r.json())));
}

Web Workers (navegador) — paralelismo real

// worker.js
self.onmessage = (event) => {
  let resultado = 0;
  for (let i = 0; i < event.data; i++) resultado += Math.sqrt(i);
  self.postMessage({ data: resultado });
};
// main.js
const worker = new Worker("./worker.js", { type: "module" });
worker.postMessage(10_000_000);
worker.onmessage = (event) => console.log("Resultado:", event.data);

worker_threads (Node.js)

import { Worker } from "worker_threads";
import { cpus } from "os";

function procesarEnWorkers(numeros) {
  const cores = cpus().length;
  const chunkSize = Math.ceil(numeros.length / cores);
  const workers = Array.from({ length: cores }, (_, i) => {
    const chunk = numeros.slice(i * chunkSize, (i + 1) * chunkSize);
    return new Promise((resolve, reject) => {
      const worker = new Worker("./heavy.mjs", { workerData: chunk });
      worker.on("message", resolve);
      worker.on("error", reject);
    });
  });
  return Promise.all(workers).then((r) => r.flat());
}

cluster (Node.js) — múltiples procesos

import cluster from "cluster";
import os from "os";
if (cluster.isPrimary) {
  os.cpus().forEach(() => cluster.fork());
  cluster.on("exit", () => cluster.fork());
} else {
  import http from "http";
  http.createServer((req, res) => res.end(`Worker ${process.pid}`)).listen(3000);
}

¿Cuál usar?

EscenarioModelo
I/O-bound (HTTP, BD, archivos)Event loop + async/await
Cálculo intensivo en frontendWeb Workers
Cálculo intensivo en backendworker_threads
Comandos del sistemachild_process
Maximo rendimiento HTTPcluster

🔗 Para saber más


Testing y calidad

Frameworks y herramientas

FrameworkPropositoAsyncCLI
node:test (Node 20+)Unitario, integración✅node --test
VitestUnitario, integración, cobertura✅npx vitest
JestUnitario, integración, mocks✅npx jest
PlaywrightE2E (navegador)✅npx playwright test
SupertestTesting de APIs HTTP✅npx vitest (integración)

Cómo testear cada concepto

Variables y tipos — aserciones simples:

import assert from "node:assert/strict";
assert.strictEqual(typeof 42, "number");
assert.strictEqual([..."😀"].length, 1);

Control de flujo — cobertura de ramas:

import assert from "node:assert/strict";
function clasificar(edad) {
  if (edad < 12) return "niño";
  if (edad < 18) return "adolescente";
  return "adulto";
}
assert.strictEqual(clasificar(10), "niño");
assert.strictEqual(clasificar(15), "adolescente");
assert.strictEqual(clasificar(30), "adulto");

POO — mocks y fixtures:

import assert from "node:assert/strict";
class MockDB {
  constructor() { this.data = []; }
  find(id) { return this.data.find(d => d.id === id) ?? null; }
}
const db = new MockDB();
db.data.push({ id: 1, nombre: "test" });
assert.strictEqual(db.find(1).nombre, "test");

Async — test con timeouts:

import assert from "node:assert/strict";
async function fetchWithTimeout(url, ms) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try { return await fetch(url, { signal: controller.signal }); }
  finally { clearTimeout(timer); }
}
await assert.rejects(
  () => fetchWithTimeout("https://httpbin.org/delay/5", 100),
  { name: "AbortError" }
);

File I/O — directorios temporales:

import assert from "node:assert/strict";
import fs from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
const dir = await fs.mkdtemp(join(tmpdir(), "test-"));
await fs.writeFile(join(dir, "test.txt"), "contenido");
const data = await fs.readFile(join(dir, "test.txt"), "utf8");
assert.strictEqual(data, "contenido");

Tests parametrizados

import assert from "node:assert/strict";

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

const casos = [
  [1, 2, 3],
  [-1, 1, 0],
  [0, 0, 0],
  [1.5, 2.5, 4],
];
for (const [a, b, esperado] of casos) {
  assert.strictEqual(sumar(a, b), esperado);
}

Property-based testing (fast-check)

npm install -D fast-check
import fc from "fast-check";
import assert from "node:assert/strict";

function ordenar(arr) { return [...arr].sort((a, b) => a - b); }

fc.assert(
  fc.property(fc.array(fc.integer()), (arr) => {
    const result = ordenar(arr);
    // Propiedades que debe cumplir cualquier ordenacion:
    assert.strictEqual(result.length, arr.length); // misma longitud
    for (let i = 1; i < result.length; i++) {
      assert.ok(result[i - 1] <= result[i]); // ordenado
    }
  })
);

Cobertura y CI

# Node --experimental-coverage (Node 22+)
node --experimental-coverage --test

# Vitest con c8
npx vitest --coverage
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm test
      - run: npm run lint

🔗 Para saber más


Conceptos avanzados

Web Workers (paralelismo en el navegador)

Los Web Workers ejecutan JavaScript en un hilo separado, permitiendo cálculos intensivos sin bloquear la UI. Se comunican con el hilo principal mediante postMessage.

// 📁 worker.js
self.onmessage = function (event) {
  const { tipo, data } = event.data;
  if (tipo === "fibonacci") {
    function fib(n) { return n <= 1 ? n : fib(n - 1) + fib(n - 2); }
    self.postMessage({ tipo: "resultado", data: fib(data) });
  }
};

// 📁 main.js
const worker = new Worker("./worker.js", { type: "module" });
worker.postMessage({ tipo: "fibonacci", data: 40 });
worker.onmessage = (event) => console.log("Fibonacci:", event.data.data);

Transferencia de datos eficiente:

// Transferir ArrayBuffer sin copiar (Transferable objects)
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
worker.postMessage({ buffer }, [buffer]); // buffer se transfiere, no se copia
// El original queda vacio (buffer.byteLength === 0)

Límites:

  • No acceso al DOM, window, document, parent
  • No alert(), confirm(), console.log() en algunos workers
  • Pueden usar fetch(), IndexedDB, WebAssembly, Canvas (OffscreenCanvas)
  • Los datos entre workers se serializan con Structured Clone

🔗 Para saber más

WebAssembly (WASM)

WebAssembly es un formato binario de bajo nivel que se ejecuta cerca de la velocidad nativa. JS puede cargar módulos WASM y llamar a sus funciones.

// Cargar modulo WASM
const response = await fetch("modulo.wasm");
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {
  env: { jsFunction: (x) => console.log("desde WASM:", x) },
});
console.log(instance.exports.wasmFunction(42));

// WASM + JS con Emscripten
// Compilar C/C++ a WASM: emcc main.c -o main.js -s EXPORTED_FUNCTIONS=['_sumar']
// Esto genera main.wasm + main.js (glue code)

Casos de uso:

  • Procesamiento de imágenes/video (Sharp usa libvips compilado a nativo, pero hay versiones WASM)
  • Juegos (Unity, Unreal Engine exportan a WASM)
  • Cálculo científico, compresión, parsing, cifrado
  • Ejecución de código no confiable en sandbox

Limitaciones:

  • Sin acceso directo al DOM (debe llamar a JS)
  • Sin acceso a APIs del sistema (archivos, red)
  • Memoria lineal (un solo ArrayBuffer grande)
  • Depuración limitada

🔗 Para saber más

Server Components (React Server Components)

Los Server Components permiten ejecutar componentes de React en el servidor, enviando solo HTML/stream al cliente. Reducen el JavaScript enviado al navegador.

// 📁 ServerComponent.js (ejecutado en servidor, NO en cliente)
import db from "./database";

export default async function PostList() {
  const posts = await db.query("SELECT * FROM posts ORDER BY creado DESC LIMIT 10");
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <h2>{post.titulo}</h2>
          <p>{post.contenido}</p>
        </li>
      ))}
    </ul>
  );
}

// 📁 ClientComponent.js (ejecutado en cliente, con interactividad)
"use client";
import { useState } from "react";

export default function LikeButton({ postId }) {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes(l => l + 1)}>Like ({likes})</button>;
}

Beneficios:

  • Acceso directo a BD, archivos, APIs sin exponer credenciales
  • Cero JavaScript enviado al cliente para componentes puramente servidores
  • Streaming: el servidor envía HTML progresivamente
  • Los Server Components pueden ser async (uso directo de async/await)

Frameworks que lo implementan:

  • Next.js (App Router): Server Components por defecto
  • Astro: islas de interactividad (server-first)
  • Remix: loader/action en servidor, componente en cliente

🔗 Para saber más

Temporal API (fechas moderna)

Temporal es la nueva API de fechas para JavaScript (Stage 3 en TC39, disponible en polyfill). Reemplaza al obsoleto objeto Date con tipos inmutables, zonas horarias nativas, calendarios y aritmética precisa.

// Temporal sigue siendo Stage 3 - usa polyfill
import { Temporal } from "temporal-polyfill";

// Crear fechas
const ahora = Temporal.Now.plainDateTimeISO();
const fecha = Temporal.PlainDate.from("2026-06-30");
const hora = Temporal.PlainTime.from("14:30:00");
const zdt = Temporal.ZonedDateTime.from({
  timeZone: "Europe/Madrid",
  year: 2026, month: 6, day: 30, hour: 14,
});

// Aritmetica
fecha.add({ days: 7 });       // 2026-07-07
fecha.subtract({ months: 1 }); // 2026-05-30
fecha.until(Temporal.PlainDate.from("2026-12-25"));
// { days: 178, months: 5, ... }

// Comparacion
Temporal.PlainDate.compare(fecha, Temporal.PlainDate.from("2026-01-01")); // 1 (posterior)

// Zonas horarias
zdt.withTimeZone("America/New_York");

// Duracion
const duracion = Temporal.Duration.from({ hours: 48, minutes: 30 });
duracion.round({ largestUnit: "days", smallestUnit: "hours" });
// => { days: 2, hours: 0, minutes: 30 }

// Formateo
fecha.toLocaleString("es-ES"); // "30/6/2026"

Ventajas sobre Date:

  • Inmutable: todas las operaciones devuelven nuevos objetos
  • Zonas horarias nativas: ZonedDateTime con IANA timezones
  • Calendarios: soporta calendarios no gregorianos
  • Aritmética precisa: add/subtract respetan meses de distinta duración
  • API coherente: from(), compare(), until(), round()

🔗 Para saber más


Proyecto final integrador

Gestor de contraseñas CLI

Combina file system, cifrado, POO, módulos, y testing en un proyecto ejecutable.

#!/usr/bin/env node
// password-manager.mjs
import { readFileSync, writeFileSync, existsSync } from "fs";
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
import { createInterface } from "readline";
import { stdin, stdout } from "process";

const ALGORITHM = "aes-256-gcm";
const VAULT_FILE = "vault.enc";
const KEY_FILE = "vault.key";

function getOrCreateKey() {
  if (existsSync(KEY_FILE)) return readFileSync(KEY_FILE);
  const key = randomBytes(32);
  writeFileSync(KEY_FILE, key);
  return key;
}

function encrypt(text, key) {
  const iv = randomBytes(16);
  const cipher = createCipheriv(ALGORITHM, key, iv);
  let encrypted = cipher.update(text, "utf8", "hex");
  encrypted += cipher.final("hex");
  return iv.toString("hex") + ":" + encrypted + ":" + cipher.getAuthTag().toString("hex");
}

function decrypt(text, key) {
  const parts = text.split(":");
  const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(parts[0], "hex"));
  decipher.setAuthTag(Buffer.from(parts[2], "hex"));
  return decipher.update(parts[1], "hex", "utf8") + decipher.final("utf8");
}

class Vault {
  #key;
  constructor() { this.#key = getOrCreateKey(); }
  getEntries() {
    if (!existsSync(VAULT_FILE)) return {};
    const encrypted = readFileSync(VAULT_FILE, "utf8");
    if (!encrypted.trim()) return {};
    return JSON.parse(decrypt(encrypted, this.#key));
  }
  saveEntries(entries) {
    writeFileSync(VAULT_FILE, encrypt(JSON.stringify(entries), this.#key));
  }
  add(service, username, password) {
    const entries = this.getEntries();
    entries[service] = { username, password, creado: new Date().toISOString() };
    this.saveEntries(entries);
    console.log(`Credenciales para '${service}' guardadas`);
  }
  get(service) {
    const entries = this.getEntries();
    if (!entries[service]) return console.log(`'${service}' no encontrado`);
    const { username, password } = entries[service];
    console.log(`Usuario: ${username}\nPassword: ${password}`);
  }
  list() {
    const services = Object.keys(this.getEntries());
    services.forEach(s => console.log(`  - ${s}`));
  }
}

function ask(query) {
  const rl = createInterface({ input: stdin, output: stdout });
  return new Promise((resolve) => rl.question(query, (answer) => { rl.close(); resolve(answer); }));
}

async function main() {
  const vault = new Vault();
  const cmd = process.argv[2];
  if (cmd === "add") {
    vault.add(await ask("Servicio: "), await ask("Usuario: "), await ask("Password: "));
  } else if (cmd === "get") vault.get(process.argv[3] || await ask("Servicio: "));
  else if (cmd === "list") vault.list();
  else console.log("Comandos: add, get, list");
}

main().catch(console.error);

Tests (password-manager.test.mjs):

import assert from "node:assert/strict";

// Test de cifrado
function encrypt(text, key) {
  const { randomBytes, createCipheriv } = await import("crypto");
  const iv = randomBytes(16);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  let e = cipher.update(text, "utf8", "hex");
  e += cipher.final("hex");
  return iv.toString("hex") + ":" + e + ":" + cipher.getAuthTag().toString("hex");
}

// Test basico de Vault
const key = Buffer.alloc(32, 0xff);
const encrypted = encrypt("test-data", key);
assert.ok(encrypted.includes(":"), "Debe contener separadores");
assert.ok(encrypted.length > 0, "No debe estar vacio");

Proyecto integrador: funciones

ComponenteImplementación
File systemLectura/escritura de vault.enc y vault.key
AlgoritmoAES-256-GCM (cifrado simétrico)
POOClase Vault con campos privados #key
TestingTests de cifrado/descifrado
Red (opcional)Compartir vault por red o sincronizar

🔗 Para saber más


Despliegue a producción

Build para producción

npm run build

Docker multi-stage

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
CMD ["node", "dist/index.js"]

Hosting

  • Vercel — despliegue desde GitHub con cero configuración
  • Netlify — integración continua con tu repo
  • Railway — conecta el repo y despliega al instante

CI/CD (GitHub Actions)

name: Deploy
on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - run: npm ci
      - run: npm test
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions@1.5
        with:
          args: "deploy"

Canales y recursos en español

YouTube

  • Midulive — JavaScript, Node.js, fullstack con proyectos prácticos
  • HolaMundo — Tutoriales cortos sobre JS, React y tecnologías web
  • MoureDev — JavaScript, automatización, proyectos reales
  • Fazt — Cursos completos de JS, React, Node.js, TypeScript
  • Sergie Code — JavaScript moderno, React, Next.js
  • CodigoFacilito — Cursos estructurados de JavaScript
  • Carlos Azaustre — JavaScript, React, Node.js con enfoque profesional
  • jonmircha — Cursos gratuitos de JavaScript desde cero
  • Victor Robles — Desarrollo web con JavaScript y Node.js

Comunidades

  • r/javascript (Reddit) — Noticias, discusiones, proyectos
  • r/Node (Reddit) — Enfocado en Node.js
  • Stack Overflow en español — Etiqueta javascript
  • Discord JavaScript en español — Canales de ayuda y proyectos

Repositorios destacados

Newsletters


Hacks y tips de productividad

1. console.table() — tablas formateadas

console.table([{ nombre: "Ana", edad: 30 }, { nombre: "Luis", edad: 25 }]);

2. structuredClone() — deep copy nativa (ES2023)

const copia = structuredClone(original); // soporta Date, Map, Set, ArrayBuffer

3. ?? (Nullish coalescing) — solo para null/undefined

const valor = 0;
const r1 = valor || "default";  // "default" (0 es falsy)
const r2 = valor ?? "default";  // 0 (solo null/undefined)

4. ?. (Optional chaining) — navegación segura

const calle = usuario?.perfil?.direccion?.calle; // undefined si falta algo

5. Campos privados # — encapsulación real

class CuentaBancaria { #saldo = 0; depositar(c) { this.#saldo += c; } }

6. Proxy para validación

function crearModeloValidado(esquema) {
  return new Proxy({}, { set(t, p, v) {
    if (esquema[p]?.required && v == null) throw new Error(`${p} requerido`);
    if (esquema[p]?.type === "number" && typeof v !== "number") throw new TypeError();
    return Reflect.set(t, p, v);
  }});
}

7. Debounce y Throttle

function debounce(fn, delay = 300) {
  let timer = null;
  return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
}
function throttle(fn, limit = 300) {
  let inThrottle = false;
  return (...args) => { if (!inThrottle) { fn(...args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } };
}

8. globalThis — objeto global universal

console.log(globalThis === window); // true en navegador
console.log(globalThis === global); // true en Node.js

9. AbortController — cancelar promesas

async function fetchConTimeout(url, ms = 5000) {
  const c = new AbortController();
  const t = setTimeout(() => c.abort(), ms);
  try { return await fetch(url, { signal: c.signal }); }
  finally { clearTimeout(t); }
}

10. Intl — internacionalización nativa

new Intl.NumberFormat("es-ES", { style: "currency", currency: "EUR" }).format(1234.5);
// "1.234,50 EUR"
new Intl.DateTimeFormat("es-ES", { dateStyle: "full" }).format(new Date());

🔗 Para saber más


⚠️ Errores comunes

ErrorCausaSolución
Cannot read properties of undefined (reading 'x')Accedes a una propiedad de un objeto que es undefined sin comprobar su existenciaUsa optional chaining (obj?.x), inicializa el objeto con valores por defecto, o valida antes de acceder
SyntaxError: Unexpected tokenSintaxis mal formada: coma colgante, paréntesis sin cerrar, spread mal usado, o sintaxis moderna en entorno legacyRevisa la línea indicada; si usas ... o ?? en Node viejo, ajusta el target de Babel o actualiza Node
ReferenceError: X is not definedVariable usada sin declarar (let, const, var) o fuera de su scopeDeclara la variable antes de usarla; comprueba que no esté confundida con un nombre de propiedad o global
Resultado inesperado con + entre string y númeroJavaScript aplica coerción de tipos: "5" + 3 devuelve "53" en vez de 8Usa Number(), parseInt() o +variable antes de la operación; o usa template literals para concatenar intencionalmente
await is only valid in async functionsSe usa await en una función que no está declarada como asyncAñade async antes del function o convierte la función a arrow async: const fn = async () => { ... }
GET ... 404 al usar fetch desde Node.js o SSRSe intenta acceder a una URL relativa (/api/datos) desde el servidor donde no hay originUsa la URL completa (https://midominio.com/api/datos) o import.meta.env.SITE en Astro/Vite para construir la URL absoluta
Promesa que nunca se resuelve (memory leak)Se crea una promesa con new Promise() pero no se llama resolve ni reject en todas las ramasAsegúrate de que todas las ramas del código asíncrono terminen llamando a resolve o reject; usa Promise.race con timeout para evitar bloqueos

🔗 Guías relacionadas

Complementa tu conocimiento de JavaScript con estas guías:


Referencias y documentación oficial

Fuentes de esta guia


Retos Relacionados

Pon a prueba lo aprendido con estos desafíos:

COMPARTIR:
COMENTARIOS:

📋 Contenido