WeakMap y WeakRef en JavaScript: referencias débiles para evitar memory leaks

WeakMap, WeakSet y WeakRef son variantes de las colecciones de ES6 diseñadas para casos donde necesitas asociar datos a objetos sin impedir que el recolector de basura los limpie cuando ya no se referencian desde ningún otro lugar. Son herramientas de nicho, pero imprescindibles cuando los memory leaks son un problema real.

WeakMap: datos asociados a objetos sin retener en memoria

Un WeakMap solo acepta objetos como claves. Si el objeto usado como clave ya no tiene ninguna referencia fuerte, el recolector de basura puede eliminarlo junto con el dato asociado en el WeakMap:

const datosPrivados = new WeakMap();

function crearUsuario(nombre, edad) {
  const usuario = {};
  datosPrivados.set(usuario, { nombre, edad, token: Math.random() });
  return usuario;
}

function getNombre(usuario) {
  return datosPrivados.get(usuario)?.nombre;
}

let u = crearUsuario('Ana', 30);
console.log(getNombre(u));  // 'Ana'

u = null;
// Ahora el objeto original no tiene referencias fuertes
// El GC puede limpiar tanto el objeto como su entrada en WeakMap

// WeakMap no es iterable (no puedes listar sus entradas)
// No tiene .size, .keys(), .values() ni .entries()

Caso real: caché de resultados DOM

Guardar datos calculados asociados a nodos del DOM sin impedir que el navegador libere la memoria cuando los nodos se eliminan:

const cache = new WeakMap();

function obtenerAltura(elemento) {
  if (cache.has(elemento)) {
    return cache.get(elemento);
  }
  const altura = elemento.getBoundingClientRect().height;
  cache.set(elemento, altura);
  return altura;
}

// Si el elemento se elimina del DOM y no hay otras referencias,
// el GC puede limpiar su entrada en el WeakMap automáticamente

Datos privados con WeakMap (patrón clásico pre-#)

Antes de los campos privados con #, WeakMap era la única forma de tener datos realmente privados en una clase:

const _privado = new WeakMap();

class CuentaBancaria {
  constructor(titular, saldo) {
    _privado.set(this, { saldo, historial: [] });
    this.titular = titular;
  }

  depositar(cantidad) {
    const datos = _privado.get(this);
    datos.saldo += cantidad;
    datos.historial.push({ tipo: 'deposito', cantidad });
  }

  get saldo() {
    return _privado.get(this).saldo;
  }
}

const cuenta = new CuentaBancaria('Ana', 1000);
cuenta.depositar(500);
console.log(cuenta.saldo);    // 1500
console.log(cuenta._privado); // undefined — no accesible

WeakSet: rastrear objetos sin retenerlos

WeakSet funciona como Set pero solo para objetos, con las mismas garantías de no retención en memoria:

const procesados = new WeakSet();

function procesar(objeto) {
  if (procesados.has(objeto)) {
    console.log('Ya procesado, saltando');
    return;
  }
  // ... procesamiento
  procesados.add(objeto);
  console.log('Procesado');
}

const pedido = { id: 42, total: 150 };
procesar(pedido);  // 'Procesado'
procesar(pedido);  // 'Ya procesado, saltando'

// Detectar referencias circulares:
function esCircular(obj, visto = new WeakSet()) {
  if (typeof obj !== 'object' || obj === null) return false;
  if (visto.has(obj)) return true;
  visto.add(obj);
  return Object.values(obj).some(v => esCircular(v, visto));
}

WeakRef y FinalizationRegistry

WeakRef crea una referencia débil a un objeto: no impide que el GC lo limpie. Para obtener el objeto, llamas a .deref(), que devuelve el objeto si aún existe o undefined si fue recogido:

let objeto = { datos: 'importantes', tamaño: 'grande' };
const ref = new WeakRef(objeto);

// Acceder al objeto:
console.log(ref.deref()?.datos);  // 'importantes'

objeto = null;  // Eliminamos la referencia fuerte
// En algún momento el GC limpiará el objeto
// ref.deref() devolverá undefined después

// FinalizationRegistry: callback cuando un objeto es recogido
const registro = new FinalizationRegistry((valor) => {
  console.log(`Objeto ${valor} fue recogido por el GC`);
});

let recurso = { nombre: 'conexión BD' };
registro.register(recurso, 'conexión BD');

recurso = null;
// Cuando el GC limpie 'recurso', imprimirá el mensaje

Cuándo usar WeakMap vs Map

La elección depende de si necesitas que el ciclo de vida de los datos esté ligado al ciclo de vida del objeto clave:

// USA Map cuando:
// - Necesitas iterar sobre las entradas
// - Las claves pueden ser primitivos
// - El ciclo de vida de los datos es independiente del objeto
const usuarios = new Map();  // Registro global de usuarios

// USA WeakMap cuando:
// - Las claves son siempre objetos
// - Quieres que los datos se limpien automáticamente con el objeto
// - Es metadata o caché asociada a objetos DOM o instancias
const metadata = new WeakMap();  // Datos de nodos DOM

En la mayoría del código de aplicación no necesitarás estas estructuras. Son relevantes en librerías, en código que gestiona muchos objetos con ciclo de vida variable, o cuando ves memory leaks relacionados con retención de objetos que deberían haberse liberado.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP