learning-front

Extra · JavaScript a fondo (opcional)

Símbolos y metaprogramación

Claves únicas que no chocan (Symbol), colecciones de claves débiles (WeakMap/WeakSet), propiedades que se calculan solas (getters/setters) y un vistazo a interceptar accesos con Proxy y Reflect.

Las clases del nivel 2 te dejaron modelar objetos con soltura. Este capítulo baja un piso más, al modelo de objetos que hay debajo: claves que nunca colisionan (Symbol), colecciones que no retienen memoria (WeakMap y WeakSet), propiedades que se calculan solas (getters/setters) y la capacidad de interceptar lo que se hace con un objeto (Proxy y Reflect). Es la maquinaria que usan por dentro librerías que ya conoces de oídas; verla te quita la sensación de magia.

Seguimos con el Team Builder. El hilo es una ficha de héroe: un winrate que siempre cuadra y unas notas internas del scout que no deben filtrarse al backend.

Symbol: una clave que nunca colisiona#

Un Symbol es un valor primitivo único: cada vez que llamas a Symbol() obtienes uno irrepetible, aunque le pongas la misma descripción. Esa descripción es solo una etiqueta para depurar; no afecta a la identidad.

javascript
// un símbolo nuevo
const a = Symbol("rol");
// otro símbolo, con la MISMA descripción
const b = Symbol("rol");
// false: dos símbolos nunca son iguales entre sí
console.log(a === b);

¿Para qué sirve algo que nunca es igual a otra cosa? Para usarlo como clave de objeto que no choca con ninguna otra. Si dos librerías guardan datos en tu objeto con la clave "id", se pisan; con un Symbol, cada una tiene su clave única. Y hay un bonus: las claves de tipo Symbol quedan fuera de Object.keys y de JSON.stringify, así que son perfectas para metadatos ocultos:

javascript
// nuestra clave para datos internos
const INTERNO = Symbol("interno");
const heroe = { nombre: "Tracer", rol: "Daño" };
// guardamos metadatos bajo la clave simbólica
heroe[INTERNO] = { revisadoPor: "scout-3" };

// ['nombre', 'rol']  -> la clave Symbol no se enumera
console.log(Object.keys(heroe));
// {"nombre":"Tracer","rol":"Daño"}  -> tampoco se serializa
console.log(JSON.stringify(heroe));
// { revisadoPor: 'scout-3' }  -> pero sigue ahí, si tienes el símbolo
console.log(heroe[INTERNO]);
// `Object.getOwnPropertySymbols` devuelve un array con las claves de tipo Symbol
// de un objeto, en orden de creación: las recupera aunque no aparezcan en Object.keys.
// [Symbol(interno)]  -> recuperable: oculto no es privado
console.log(Object.getOwnPropertySymbols(heroe));

Un matiz que conviene fijar: quedar fuera de Object.keys y JSON.stringify hace la clave oculta a la enumeración, no privada. Quien tenga el símbolo —o lo rescate con Object.getOwnPropertySymbols— puede leerla. Para datos que de verdad no deberían vivir dentro del objeto, el WeakMap de la siguiente sección es la opción limpia.

En el capítulo anterior ya usaste uno sin saber su nombre: Symbol.iterator es un well-known symbol, una clave simbólica que el lenguaje trae incorporada para el protocolo de iteración. Si tu objeto implementa un método bajo esa clave, for...of sabe recorrerlo.

WeakMap y WeakSet: datos atados a un objeto, sin fugas#

Ya conoces Map y Set del nivel 2. Sus primos débiles se parecen, con dos diferencias que los hacen ideales para asociar datos a objetos sin estorbar:

Un WeakMap es como un Map, pero sus claves solo pueden ser objetos y las guarda con una referencia débil: si ese objeto deja de usarse en el resto del programa, su entrada en el WeakMap se libera sola. Sirve para guardar datos privados asociados a un objeto sin mantenerlo vivo a la fuerza:

javascript
// las claves serán objetos héroe
const notas = new WeakMap();
const tracer = { nombre: "Tracer" };

// asociamos una nota a ESE objeto
notas.set(tracer, "Agresiva; brilla en control");
// 'Agresiva; brilla en control'
console.log(notas.get(tracer));
// La nota no es una propiedad de tracer: vive fuera. Si tracer dejara de usarse,
// su entrada en el WeakMap se descartaría sin que tú hagas nada (cero fugas).

Un WeakSet es el equivalente de Set: solo guarda objetos, con referencias débiles. Su uso típico es marcar objetos sin alterarlos —“este héroe ya pasó la validación”, “este nodo ya se procesó”— sin impedir que se liberen luego:

javascript
// un conjunto de objetos "ya validados"
const validados = new WeakSet();
const ana = { nombre: "Ana" };

// marcamos a ana como validada
validados.add(ana);
// true: ¿está marcada? sí
console.log(validados.has(ana));
// No hemos tocado el objeto ana: la marca vive en el WeakSet, aparte.

Lo “débil” es la clave: no son para recorrer (no se pueden iterar) ni para listar su contenido; son para asociar y consultar datos por objeto sin crear fugas de memoria.

El otro uso que verás todo el rato es una caché por objeto: guardar el resultado de un cálculo caro la primera vez y reutilizarlo después, sin ensuciar el objeto ni arriesgar una fuga.

javascript
// asocia cada héroe con un resultado ya calculado
const cache = new WeakMap();

function statsCaras(heroe) {
  // ya calculado antes: lo reutilizamos
  if (cache.has(heroe)) return cache.get(heroe);
  // imagina aquí un cálculo costoso
  const resultado = heroe.victorias / heroe.partidas;
  // lo guardamos asociado a ESTE héroe, para la próxima
  cache.set(heroe, resultado);
  return resultado;
}

const tracer = { nombre: "Tracer", victorias: 78, partidas: 120 };
// 0.65  -> primera vez: lo calcula y lo cachea
console.log(statsCaras(tracer));
// 0.65  -> segunda vez: lo saca de la caché, sin recalcular
console.log(statsCaras(tracer));
// Si 'tracer' deja de usarse en el programa, su entrada en la caché se libera sola.

Getters y setters: propiedades que se calculan solas#

Un getter (get) es un método que se lee como si fuera una propiedad. No guarda un valor: lo calcula cada vez que se lee. Eso lo hace perfecto para datos derivados como el winrate, que nunca debe quedarse desfasado:

javascript
const heroe = {
  victorias: 78,
  partidas: 120,
  get winrate() {
    // se ejecuta al leer heroe.winrate, no al definirlo
    // siempre desde los datos actuales
    return this.victorias / this.partidas;
  },
};

// 0.65  -> se lee SIN paréntesis, como una propiedad
console.log(heroe.winrate);
// cambiamos el dato base
heroe.victorias = 90;
heroe.partidas = 130;
// 0.692…  -> recalculado solo, sin tocar nada más
console.log(heroe.winrate);

Su pareja, el setter (set), corre cuando asignas a la propiedad. Sirve para validar o transformar antes de guardar:

javascript
const heroe = {
  // el valor real; el guion bajo es una convención de "no me toques directo"
  _rango: "Oro",
  get rango() {
    // leer heroe.rango devuelve el guardado
    return this._rango;
  },
  set rango(nuevo) {
    // corre al hacer heroe.rango = '...'
    const validos = ["Bronce", "Plata", "Oro", "Platino", "Diamante"];
    if (!validos.includes(nuevo)) {
      // rechazamos lo que no toca
      console.log("Rango inválido: " + nuevo);
      // no guardamos basura
      return;
    }
    // valor válido: lo guardamos
    this._rango = nuevo;
  },
};

// pasa la validación
heroe.rango = "Platino";
// 'Platino'
console.log(heroe.rango);
// no existe -> el setter lo rechaza
heroe.rango = "Leyenda";
// sigue 'Platino'
console.log(heroe.rango);

Funcionan igual dentro de una clase (las del nivel 2): un get winrate() entre los métodos se usa como instancia.winrate, sin paréntesis. Es la forma estándar de exponer valores calculados en una API de objeto limpia.

Proxy y Reflect: interceptar los accesos#

Lo más avanzado del capítulo: el objetivo es reconocerlo y saber qué resuelve, no implementarlo desde cero. Un Proxy envuelve un objeto y te deja interceptar lo que se hace con él —leer una propiedad, escribirla, comprobar si existe— mediante funciones llamadas traps. El ejemplo clásico: registrar cada lectura.

javascript
const datos = { nombre: "Mercy", rol: "Apoyo" };

// new Proxy(objetoReal, traps). La trap 'get' se dispara en CADA lectura.
const observado = new Proxy(datos, {
  get(obj, clave) {
    // nuestra lógica: registrar
    console.log("acceso a: " + String(clave));
    // …y delegar la lectura real en Reflect
    return Reflect.get(obj, clave);
  },
});

// "acceso a: nombre", luego "Mercy"
console.log(observado.nombre);
// "acceso a: rol", luego "Apoyo"
console.log(observado.rol);

Reflect es el compañero del Proxy: ofrece las operaciones internas sobre objetos como funciones normales (Reflect.get, Reflect.set, Reflect.has). Dentro de una trap lo usas para delegar en el comportamiento por defecto sin reimplementarlo: tú añades tu lógica (registrar, validar) y Reflect hace el trabajo de siempre.

El ejemplo de arriba solo observa, pero una trap también puede decidir qué devolver. Un caso real: un diccionario de traducciones (i18n) que, cuando le pides una clave que no existe, responde con un aviso claro en vez de un undefined que acabaría pintándose en la pantalla:

javascript
// las traducciones que tenemos
const textos = { saludo: "Hola", adios: "Adiós" };

const t = new Proxy(textos, {
  get(obj, clave) {
    // existe: la traducción de siempre
    if (clave in obj) return obj[clave];
    // no existe: aviso, nunca un undefined silencioso
    return "[falta: " + String(clave) + "]";
  },
});

// 'Hola'  -> la clave existe
console.log(t.saludo);
// '[falta: cerrar]'  -> la trap devuelve el aviso, no undefined
console.log(t.cerrar);

Cambiar lo que pasa al leer una clave que falta —en vez de tragarte un undefined— es metaprogramación de la útil: el objeto que envuelves no cambia, pero su comportamiento desde fuera sí.

Esto, multiplicado, es el motor de la reactividad de Vue 3 y MobX: envuelven tus datos en un Proxy, detectan qué lees durante un render y, cuando algo cambia, vuelven a pintar solo lo que dependía de ese dato. La idea importante: las librerías hacen esto por ti; tú trabajas con objetos normales. Y al revés: no metaprogramar por deporte. Un Proxy donde basta un get es complejidad que alguien tendrá que mantener.

Pruébalo tú#

En la consola: un Symbol que no colisiona y se esconde del JSON, un get winrate que se actualiza solo al cambiar los datos, un Proxy que registra cada lectura y otro que devuelve un valor por defecto cuando falta una clave. Cambia las victorias de tracer y mira cómo el winrate se recalcula; lee otra propiedad del espia y observa el registro; pide a t una clave que no exista y mira el aviso. Pulsa Ejecutar (o Ctrl+Enter) para ver la consola.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué garantiza `Symbol()`?

Tu turno#

Modela la ficha del héroe: winrate que siempre cuadra, nota del scout que no se filtra al backend, y una guardia en registrarVictoria con el WeakSet que el starter ya te da para evitar contar la misma victoria dos veces. Empieza por la versión directa y sube de nivel con un getter y una clave privada. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo el nivel Excelente saca los datos privados a un WeakMap y razona cuándo esa maquinaria sobra.

Ejercicio · en esta página

Héroe con winrate calculado y metadatos privados

Convierte el roster en bruto en objetos de la app donde el winrate se calcule solo (siempre coherente con victorias y partidas) y la nota del scout quede privada: no debe aparecer en lo que se serializa para el backend (JSON.stringify), pero sí seguir accesible para el equipo. Además, usa el WeakSet `procesados` del starter para que `registrarVictoria` no pueda registrar la misma victoria dos veces en la misma sesión.

Paso 1: Que funcione

  • Se muestra la plantilla por la consola con su winrate y la nota del scout.
  • Vale calcular el winrate como una propiedad fija y la nota como propiedad normal.
  • registrarVictoria usa el WeakSet para no contar una victoria dos veces.
  • Comprendes el problema: la nota se filtra en el JSON y el winrate se desfasa.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// winrate es una propiedad FIJA (se calcula una vez al crear el héroe) y la nota
// del scout es una propiedad normal. El resultado se ve, pero...
//   - tras registrarVictoria(Tracer), su V/P pasa a 79/121 pero el winrate sigue
//     marcando 65.0% (78/120): se quedó OBSOLETO. Habría que recalcularlo a mano
//     en cada cambio, y olvidarse una sola vez es un bug silencioso.
//   - la nota es una propiedad más, así que VIAJA en JSON.stringify: el payload
//     del backend filtra un dato interno del equipo.
// Funciona, pero deja dos trampas que getters y claves privadas resuelven de raíz.
// ════════════════════════════════════════════════════════════════════════════

const ROSTER = [
  {
    nombre: "Tracer",
    rol: "Daño",
    victorias: 78,
    partidas: 120,
    nota: "Agresiva; brilla en mapas de control",
  },
  {
    nombre: "Mercy",
    rol: "Apoyo",
    victorias: 130,
    partidas: 200,
    nota: "Pocket healer; vigilar el posicionamiento",
  },
  {
    nombre: "Reinhardt",
    rol: "Tanque",
    victorias: 51,
    partidas: 90,
    nota: "Buen escudo; rota lento",
  },
  {
    nombre: "Ana",
    rol: "Apoyo",
    victorias: 66,
    partidas: 110,
    nota: "Sleep dart decisivo; objetivo prioritario rival",
  },
];

// Conjunto de héroes cuya victoria ya se registró. WeakSet: solo objetos,
// referencias débiles; no impide que el objeto se libere si deja de usarse.
const procesados = new WeakSet();

function registrarVictoria(heroe) {
  // si ya está marcado, ignoramos el intento duplicado
  if (procesados.has(heroe)) {
    console.log(heroe.nombre + " ya procesado en esta sesión");
    return;
  }
  // lo marcamos para no repetirlo
  procesados.add(heroe);
  // una victoria más
  heroe.victorias++;
  // y una partida más
  heroe.partidas++;
}

function crearHeroe(raw) {
  return {
    nombre: raw.nombre,
    rol: raw.rol,
    victorias: raw.victorias,
    partidas: raw.partidas,
    // VALOR FIJO: se queda obsoleto si cambian las partidas
    winrate: raw.victorias / raw.partidas,
    // propiedad normal: aparece en JSON.stringify y en Object.keys
    nota: raw.nota,
  };
}

const heroes = ROSTER.map(crearHeroe);
// Tracer juega una más… pero su winrate ya no se entera
registrarVictoria(heroes[0]);

// la leemos como una propiedad cualquiera
const notaInterna = heroes[0].nota;

// muestra la plantilla en la consola
console.log("--- Plantilla ---");
for (const h of heroes) {
  console.log(
    h.nombre +
      " | " +
      h.rol +
      " | " +
      h.victorias +
      "/" +
      h.partidas +
      " | " +
      (h.winrate * 100).toFixed(1) +
      "%",
  );
}
// el JSON filtra la nota
console.log("--- JSON del primer héroe (lo que viaja al backend) ---");
console.log(JSON.stringify(heroes[0], null, 2));
console.log("--- Nota interna del scout (no debe viajar) ---");
console.log(notaInterna);

Por qué este nivel

  • winrate es una propiedad fija y la nota una propiedad normal: lo más directo. El WeakSet en registrarVictoria evita el doble registro.
  • Dos trampas: tras sumar una partida, el winrate se queda obsoleto (habría que recalcularlo a mano), y la nota interna VIAJA en JSON.stringify.