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.
