JavaScript Difícil · Teoría

Comprende antes de memorizar

24 lecciones originales con ejemplos breves, puntos clave y referencias para profundizar.

01

Closures

Un closure es una función junto con el acceso al entorno léxico donde fue creada. Conserva bindings, no copias mágicas de sus valores: si el binding cambia, las llamadas posteriores observan ese estado actualizado.

Este mecanismo permite encapsular estado y fabricar funciones configuradas. Amplía el alcance estudiado antes: la vida de un binding puede superar la ejecución que creó su entorno cuando una función todavía lo referencia.

Ejemplo
function crearSecuencia(inicio) {
  let actual = inicio;
  return () => actual++;
}
const siguiente = crearSecuencia(10);
console.log(siguiente(), siguiente());

Puntos importantes

  • Cada llamada exterior crea bindings independientes.
  • Error común: describir el closure como una fotografía inmutable del valor.
  • La encapsulación depende del alcance léxico, no de una palabra reservada especial.
Practicar este concepto
02

Lexical environment y lexical scope avanzado

La estructura léxica del código determina qué entorno exterior consulta un nombre. Durante la ejecución, los registros de entorno mantienen bindings para parámetros, declaraciones y bloques, y se enlazan con un entorno externo.

Una función busca nombres desde el lugar donde fue definida, no desde donde luego se invoca. Esto explica closures y sombreado sin imaginar objetos públicos que puedan inspeccionarse directamente.

Ejemplo
const etiqueta = "exterior";
function crear() {
  const etiqueta = "léxica";
  return () => etiqueta;
}
console.log(crear()());

Puntos importantes

  • El scope se decide por la estructura del programa.
  • Error común: esperar que el caller aporte sus variables locales.
  • El sombreado crea otro binding; no modifica automáticamente el exterior.
03

this y call-site

En una función normal, el valor de this depende de cómo se realiza la llamada: como método, con new, mediante call/apply/bind o como llamada simple. La función no queda unida para siempre al objeto donde fue escrita.

Una arrow function no crea su propio this: captura el binding léxico del entorno. Por eso call, apply y bind pueden fijar this en funciones normales, pero no reemplazan el this capturado por una arrow.

Ejemplo
function leer() { return this.nombre; }
const uno = { nombre: "uno", leer };
const dos = { nombre: "dos", leer };
console.log(uno.leer(), dos.leer());

Puntos importantes

  • Observa el call-site, no sólo la declaración.
  • Error común: separar un método y esperar que conserve su receptor.
  • Las arrows sirven para heredar this, no como reemplazo universal de method syntax.
Practicar este concepto
04

call, apply y bind

call y apply invocan una función normal de inmediato con un this explícito; call recibe argumentos separados y apply recibe una colección de argumentos. bind crea otra función que conserva el this y, opcionalmente, argumentos iniciales.

Estas operaciones hacen explícito el receptor cuando una función debe reutilizarse o viajar como callback. Sobre una arrow no sustituyen el this léxico, aunque bind todavía puede prefijar argumentos.

Ejemplo
function presentar(prefijo, cierre) {
  return `${prefijo} ${this.nombre}${cierre}`;
}
const contexto = { nombre: "Ada" };
console.log(presentar.call(contexto, "Hola", "."));

Puntos importantes

  • call y apply ejecutan; bind devuelve una función.
  • Error común: pasar un array a call cuando corresponde apply o spread.
  • Relaciona estas APIs con el call-site de la lección anterior.
Practicar este concepto
05

Prototype chain

Cuando una propiedad no es propia, JavaScript continúa la búsqueda por el prototipo del objeto y repite el proceso hasta encontrarla o llegar a null. Esa cadena delega comportamiento sin copiar cada método en cada instancia.

Object.getPrototypeOf permite observar el enlace; hasOwn distingue una propiedad propia de una heredada. La asignación normal suele crear o actualizar una propiedad propia y puede ocultar la heredada.

Ejemplo
const base = { describir() { return this.nombre.toUpperCase(); } };
const elemento = Object.create(base);
elemento.nombre = "nodo";
console.log(elemento.describir());

Puntos importantes

  • La consulta sube por la cadena; la identidad del receptor sigue siendo elemento.
  • Error común: creer que el método heredado fue copiado al objeto.
  • Este modelo es la base sobre la que operan las clases.
Practicar este concepto
06

Herencia prototípica

La herencia prototípica conecta objetos para delegar lecturas y métodos. Los constructores tradicionales exponen un objeto prototype que se convierte en prototipo de las instancias creadas con new.

Compartir métodos en el prototipo reduce duplicación, pero compartir allí estado mutable mezcla accidentalmente instancias. Cada instancia debe poseer los datos que cambian de manera independiente.

Ejemplo
function Cuenta(nombre) { this.nombre = nombre; this.movimientos = []; }
Cuenta.prototype.registrar = function (valor) { this.movimientos.push(valor); };
const cuenta = new Cuenta("Caja");

Puntos importantes

  • prototype del constructor no es lo mismo que el prototipo del constructor como función.
  • Error común: colocar arrays mutables compartidos en el prototipo.
  • Object.create ofrece delegación directa sin constructor.
07

Descriptores de propiedades

Una propiedad de datos tiene un valor y atributos writable, enumerable y configurable. Una propiedad accesora usa get y/o set en lugar de value y writable; Object.getOwnPropertyDescriptor permite inspeccionar ese contrato.

Object.defineProperty usa false como valor predeterminado para atributos omitidos. Los descriptores controlan operaciones del lenguaje, no validan por sí solos el significado del dato.

Ejemplo
const registro = {};
Object.defineProperty(registro, "id", {
  value: 7, enumerable: true, writable: false, configurable: false,
});
console.log(Object.keys(registro));

Puntos importantes

  • No mezcles value/writable con get/set en el mismo descriptor.
  • Error común: asumir que defineProperty replica los valores predeterminados de un literal.
  • La enumerabilidad afecta a Object.keys, no a la existencia de la propiedad.
08

Getters y setters

Un getter calcula una lectura con sintaxis de propiedad y un setter intercepta una asignación. Son accesores síncronos: conviene que su coste y efectos no sorprendan a quien usa el objeto.

El almacenamiento debe usar otro nombre o un campo privado. Escribir this.valor dentro del setter de valor volvería a invocar el mismo setter hasta desbordar la pila.

Ejemplo
const rango = {
  _valor: 0,
  get valor() { return this._valor; },
  set valor(nuevo) { this._valor = Math.max(0, Math.min(10, nuevo)); },
};
rango.valor = 14;

Puntos importantes

  • Los accesores se describen con get/set, no value.
  • Error común: recursión al leer o escribir la misma propiedad dentro del accesor.
  • Amplían los descriptores con una interfaz de propiedad.
Practicar este concepto
09

Clases

La sintaxis class organiza constructor, métodos, accesores y campos. Es una abstracción sobre el modelo de prototypes: los métodos de instancia se ubican en el prototype y las instancias delegan hacia él.

Las clases añaden reglas propias, como ejecución estricta y la obligación de usar new. La sintaxis resulta más declarativa, pero no convierte JavaScript en un sistema de clases desconectado de su cadena prototípica.

Ejemplo
class Medidor {
  constructor(unidad) { this.unidad = unidad; this.valor = 0; }
  sumar(cantidad) { this.valor += cantidad; return this.valor; }
}
const medidor = new Medidor("km");

Puntos importantes

  • Los métodos se comparten mediante Medidor.prototype.
  • Error común: pensar que cada instancia recibe una copia de cada método.
  • Comprender prototypes permite explicar instanceof y herencia de clases.
10

Herencia de clases: extends y super

extends enlaza tanto la cadena de instancias como la relación estática entre clases. En un constructor derivado, super(...) debe ejecutar el constructor base antes de acceder a this.

super.metodo() permite extender una implementación base sin buscarla manualmente. La herencia expresa una relación de sustitución; para combinar capacidades independientes suele ser más claro componer objetos.

Ejemplo
class Mensaje { constructor(texto) { this.texto = texto; } formato() { return this.texto; } }
class Alerta extends Mensaje {
  constructor(texto) { super(texto); }
  formato() { return `¡${super.formato()}!`; }
}

Puntos importantes

  • super() inicializa this en constructores derivados.
  • Error común: usar this antes de super().
  • extends conserva el modelo prototípico en lugar de reemplazarlo.
Practicar este concepto
11

Campos privados

Los nombres #privados se declaran en el cuerpo de la clase y sólo pueden referenciarse con esa sintaxis dentro de la clase. Son elementos privados reales con comprobación de marca (brand check), no propiedades ocultas con claves de texto.

objeto['#saldo'] no accede al campo #saldo y Object.keys no lo enumera. Intentar usar un nombre privado no declarado es un error de sintaxis; aplicarlo a un objeto sin la marca correcta produce un error.

Ejemplo
class Billetera {
  #saldo = 0;
  depositar(monto) { this.#saldo += monto; }
  consultar() { return this.#saldo; }
}
const billetera = new Billetera();

Puntos importantes

  • La privacidad se valida por sintaxis y marca de instancia.
  • Error común: confundir #dato con la clave de texto '#dato'.
  • Los closures también encapsulan, pero con un mecanismo y una forma de compartir distintos.
12

Symbols

Cada Symbol() crea un valor primitivo único que puede usarse como clave de propiedad sin colisionar con claves de texto. Su descripción ayuda a depurar, pero dos símbolos con la misma descripción siguen siendo diferentes.

Los símbolos conocidos permiten participar en protocolos del lenguaje, como Symbol.iterator. Object.keys omite claves symbol; Reflect.ownKeys incluye claves string y symbol.

Ejemplo
const estado = Symbol("estado");
const tarea = { nombre: "revisar", [estado]: "interna" };
console.log(Object.keys(tarea), Reflect.ownKeys(tarea));

Puntos importantes

  • Symbol no vuelve secreta una propiedad: puede descubrirse con APIs de reflexión.
  • Error común: comparar símbolos por su descripción.
  • Symbol.iterator conecta este tema con el protocolo iterable.
13

Iterables e iteradores

Un iterable implementa [Symbol.iterator]() y devuelve un iterador. Cada llamada a next() del iterador produce un objeto con value y done; for...of y spread consumen ese protocolo.

Separar el iterable del iterador permite crear recorridos nuevos y, si se desea, varios iteradores independientes. Un iterador agotado debe continuar indicando done: true.

Ejemplo
const tramo = {
  desde: 2, hasta: 4,
  [Symbol.iterator]() {
    let actual = this.desde; const fin = this.hasta;
    return { next: () => actual <= fin ? { value: actual++, done: false } : { done: true } };
  },
};
console.log([...tramo]);

Puntos importantes

  • El iterable inicia recorridos; el iterador entrega pasos.
  • Error común: devolver valores directos en next() en vez de { value, done }.
  • for...of consume valores y no depende de índices.
Practicar este concepto
14

Generadores

Una función generadora se declara con function* y devuelve un generador que implementa los protocolos de iterador e iterable. Cada yield suspende su ejecución y conserva su estado hasta el siguiente next().

El valor pasado a next(valor) se convierte en el resultado del yield suspendido, salvo en la primera llamada. return finaliza la secuencia y su valor no aparece normalmente en for...of.

Ejemplo
function* alternar() {
  yield "izquierda";
  yield "derecha";
}
console.log([...alternar()]);

Puntos importantes

  • Invocar la función no ejecuta inmediatamente todo su cuerpo.
  • Error común: reutilizar un generador agotado esperando que se reinicie.
  • Los generadores reducen el estado manual de muchos iteradores.
Practicar este concepto
15

Map y Set

Map asocia claves de cualquier tipo con valores y conserva orden de inserción para iteración. Set mantiene valores únicos con el mismo criterio de igualdad usado por estas colecciones.

Frente a un objeto, Map evita convertir las claves en cadenas de texto y expone size e iteración directa. Set elimina duplicados por identidad para objetos: dos literales equivalentes siguen siendo valores distintos.

Ejemplo
const visitas = new Map();
const pagina = { ruta: "/inicio" };
visitas.set(pagina, 2);
const etiquetas = new Set(["js", "js", "web"]);

Puntos importantes

  • Map usa get/set/has; no notación de punto para sus entradas.
  • Error común: esperar igualdad profunda en Set.
  • Estas colecciones hacen explícitos contratos que antes se simulaban con objetos y arrays.
Practicar este concepto
16

WeakMap y WeakSet

WeakMap mantiene asociaciones cuyas claves deben ser objetos o símbolos no registrados; WeakSet mantiene referencias débiles admitidas por su contrato. No impiden por sí solos que una clave recolectable sea liberada.

No son enumerables y no exponen size porque observar sus miembros haría depender el programa de decisiones de recolección de basura. Las pruebas deben verificar el comportamiento de has/get/delete, nunca afirmar que ocurrió GC.

Ejemplo
const metadatos = new WeakMap();
const elemento = {};
metadatos.set(elemento, { validado: true });
console.log(metadatos.get(elemento).validado);

Puntos importantes

  • Su caso típico es asociar metadatos sin controlar la vida de la clave.
  • Error común: intentar recorrer una colección débil.
  • La recolección es no determinista y no constituye una aserción válida.
17

Combinadores de Promise

Promise.all cumple con todos los valores en orden y rechaza al primer rechazo observado. allSettled siempre cumple cuando todas se asentaron y describe cada fulfillment o rejection.

Promise.race se asienta como la primera entrada que se asiente; Promise.any cumple con el primer fulfillment y sólo rechaza con AggregateError si todas rechazan. Settlement incluye cumplimiento o rechazo, no sólo finalización exitosa.

Ejemplo
const datos = await Promise.all([Promise.resolve(3), 5]);
const estados = await Promise.allSettled([Promise.resolve(1), Promise.reject("x")]);
console.log(datos, estados.map((item) => item.status));

Puntos importantes

  • Los combinadores no cancelan automáticamente las operaciones restantes.
  • Error común: confundir race con primer fulfillment; ese contrato corresponde a any.
  • Amplían las cadenas Promise del nivel medio hacia coordinación explícita.
Practicar este concepto
18

Event loop y modelo de ejecución

El stack ejecuta un job a la vez. Operaciones del host pueden registrar trabajo y, cuando corresponde, colocar callbacks en colas; el event loop del host elige la siguiente task cuando el stack queda libre.

ECMAScript especifica jobs, como reacciones de Promise, mientras navegador y Node definen tasks, timers y fases. Por eso conviene razonar stack → host → queues → event loop y separar garantías del lenguaje de detalles específicos del host.

Ejemplo
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// Orden verificado: A, D, C, B

Puntos importantes

  • El código síncrono A y D termina antes de ejecutar callbacks.
  • Error común: creer que un timer con cero demora interrumpe el stack.
  • Node posee fases y prioridades propias que no deben presentarse como reglas universales de ECMAScript.
19

Microtasks y tasks

Las reacciones de Promise y queueMicrotask se encolan como microtasks del host. Al terminar la ejecución actual, el host drena la cola de microtasks antes de avanzar a la siguiente task, incluido un callback de timer.

Una microtask puede encolar otras microtasks, de modo que una cadena sin límite puede retrasar tasks. El nombre macrotask es informal; la terminología de plataforma suele hablar simplemente de tasks.

Ejemplo
const orden = [];
orden.push("A");
queueMicrotask(() => orden.push("C"));
orden.push("D");
await Promise.resolve();
console.log(orden.join(" "));

Puntos importantes

  • Las microtasks se drenan antes de la próxima task.
  • Error común: atribuir una prioridad de microtask a todo callback asíncrono.
  • El orden A, D, C, B combina ejecución síncrona, microtask y task del host.
Practicar este concepto
20

Async/await avanzado

Cada llamada async devuelve una Promise. await obtiene un fulfillment o lanza el motivo de rechazo dentro de la función, y la continuación posterior se programa como un job en lugar de bloquear el hilo.

Esperar operaciones independientes una por una puede serializarlas innecesariamente. Iniciarlas primero y coordinarlas con Promise.all conserva concurrencia; hacerlo sólo es correcto cuando no existe dependencia entre resultados.

Ejemplo
async function resumen() {
  const perfilPendiente = Promise.resolve({ id: 4 });
  const permisosPendientes = Promise.resolve(["leer"]);
  const [perfil, permisos] = await Promise.all([perfilPendiente, permisosPendientes]);
  return { perfil, permisos };
}

Puntos importantes

  • await suspende la función async, no todo el entorno.
  • Error común: serializar trabajo independiente con awaits consecutivos.
  • La gestión del rechazo debe situarse donde exista una recuperación útil.
Practicar este concepto
21

Propagación de errores asíncronos

Un throw dentro de async rechaza la Promise devuelta. Un rechazo esperado por await vuelve a lanzarse en esa función; si no se captura, continúa por la cadena hasta un manejador de rechazo.

Capturar sólo para registrar y devolver undefined puede ocultar el contrato fallido. Es preferible recuperar con un valor válido, añadir contexto mediante cause o propagar el error para que una capa responsable decida.

Ejemplo
async function cargar() {
  try {
    return await Promise.reject(new Error("dato ausente"));
  } catch (cause) {
    throw new Error("No se pudo cargar el perfil", { cause });
  }
}

Puntos importantes

  • Un catch transforma el resultado si devuelve otro valor.
  • Error común: olvidar await dentro de try y dejar el rechazo fuera de ese catch.
  • La propagación asíncrona conserva la idea de contratos de error del nivel medio.
22

Módulos ECMAScript

Los módulos tienen su propio ámbito, dependencias declaradas y bindings importados vivos: una importación observa actualizaciones del binding exportado, aunque no permite reasignarlo desde el importador. La resolución y carga pertenecen al host.

Este playground bloquea import y export porque ejecuta fragmentos confinados sin cargar módulos, archivos o red. La lección es exclusivamente teórica: no se habilitan módulos ni se evade esa frontera para practicar la sintaxis.

Ejemplo
// contador.js
export let total = 0;
export function incrementar() { total += 1; }
// vista.js
import { total, incrementar } from "./contador.js";

Puntos importantes

  • Los imports estáticos se declaran en el nivel superior.
  • Error común: describir un binding importado como una copia fija del valor.
  • No pegues este ejemplo en el playground: import/export están bloqueados deliberadamente.
23

Inmutabilidad y Object.freeze

Object.freeze impide reconfigurar propiedades propias y vuelve no escribibles las propiedades de datos del objeto. Es superficial: un objeto o array anidado conserva su mutabilidad salvo que también se congele.

Actualizar mediante copias puede facilitar razonamiento y detección de cambios, pero spread también es superficial. La inmutabilidad profunda requiere recorrer la estructura o adoptar contratos que eviten exponer referencias mutables.

Ejemplo
const configuracion = Object.freeze({ tema: "claro", opciones: { compacto: false } });
configuracion.opciones.compacto = true;
console.log(configuracion.opciones.compacto);

Puntos importantes

  • freeze devuelve el mismo objeto.
  • Error común: asumir que congela recursivamente todo el grafo.
  • Conecta con referencias y copias superficiales del nivel medio.
24

Composición y memoización

La composición construye una operación conectando funciones pequeñas, con salidas compatibles con las entradas siguientes. Reduce jerarquías cuando el comportamiento puede combinarse sin una relación de herencia.

La memoización guarda resultados por entrada para evitar trabajo repetido, pero introduce compromisos: coste de la caché, semántica de identidad para objetos, invalidación y retención de memoria. Sólo conviene para funciones compatibles con reutilizar el resultado y cargas donde la caché aporta valor.

Ejemplo
const componer = (f, g) => (valor) => f(g(valor));
const normalizar = (texto) => texto.trim().toLowerCase();
const longitud = (texto) => texto.length;
const longitudNormalizada = componer(longitud, normalizar);

Puntos importantes

  • La composición exige contratos claros entre funciones.
  • Error común: memoizar funciones con efectos o resultados dependientes del tiempo.
  • Map usa identidad de claves; esa decisión afecta a los aciertos y a la memoria de una caché.
Practicar este concepto