Symbols en JavaScript: Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance y metaprogramación

Los Symbol son un tipo primitivo de JavaScript introducido en ES6 que genera valores únicos e irrepetibles. Cada llamada a Symbol() produce un valor distinto, incluso con la misma descripción. Son la herramienta correcta cuando necesitas claves de objeto que no colisionen con ninguna propiedad existente o futura, y los well-known symbols permiten personalizar comportamientos profundos del lenguaje.

Creación y unicidad

const s1 = Symbol('descripcion');
const s2 = Symbol('descripcion');

console.log(s1 === s2);     // false — siempre únicos
console.log(typeof s1);     // "symbol"
console.log(s1.toString()); // "Symbol(descripcion)"
console.log(s1.description); // "descripcion" (ES2019)

Symbols como claves de objeto

Al usar un Symbol como clave, la propiedad queda oculta a for...in, Object.keys() y JSON.stringify(). Solo es accesible si tienes la referencia al Symbol original:

const ID = Symbol('id');
const PERMISOS = Symbol('permisos');

const usuario = {
  nombre: 'Ana',
  [ID]: 'usr-001',
  [PERMISOS]: ['leer', 'escribir'],
};

console.log(Object.keys(usuario));    // ['nombre']
console.log(JSON.stringify(usuario)); // '{"nombre":"Ana"}'
console.log(usuario[ID]);            // 'usr-001'

// Para obtener las claves Symbol de un objeto:
console.log(Object.getOwnPropertySymbols(usuario)); // [Symbol(id), Symbol(permisos)]

Symbol.for: registro global compartido

Symbol.for(clave) busca en el registro global y devuelve el mismo Symbol si ya existe con esa clave. Es la forma de compartir Symbols entre módulos o iframes:

const A = Symbol.for('app.id');
const B = Symbol.for('app.id');

console.log(A === B); // true — mismo del registro global
console.log(Symbol.keyFor(A)); // "app.id"

// A diferencia de Symbol() normal:
const local = Symbol('app.id');
console.log(Symbol.keyFor(local)); // undefined — no está en el registro

Symbol.iterator: hacer objetos iterables

Implementar Symbol.iterator en un objeto lo hace iterable con for...of, el operador spread y la desestructuración de arrays:

class Rango {
  constructor(inicio, fin, paso = 1) {
    this.inicio = inicio;
    this.fin = fin;
    this.paso = paso;
  }

  [Symbol.iterator]() {
    let actual = this.inicio;
    const { fin, paso } = this;
    return {
      next() {
        if (actual <= fin) {
          const value = actual;
          actual += paso;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const pares = new Rango(2, 10, 2);
console.log([...pares]); // [2, 4, 6, 8, 10]

for (const n of new Rango(1, 5)) {
  process.stdout.write(n + ' '); // 1 2 3 4 5
}

Symbol.toPrimitive: conversión controlada

Symbol.toPrimitive permite definir cómo se convierte un objeto a un primitivo según el contexto ("number", "string" o "default"):

class Dinero {
  constructor(cantidad, moneda) {
    this.cantidad = cantidad;
    this.moneda = moneda;
  }

  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.cantidad;
    if (hint === 'string') return `${this.cantidad} ${this.moneda}`;
    // "default": usado en operaciones como + con string
    return this.cantidad;
  }
}

const precio = new Dinero(42.99, 'EUR');

console.log(+precio);            // 42.99  (hint: "number")
console.log(`Total: ${precio}`); // "Total: 42.99 EUR" (hint: "string")
console.log(precio + 10);        // 52.99  (hint: "default")

Symbol.hasInstance: control de instanceof

Symbol.hasInstance en una clase estática permite personalizar el comportamiento del operador instanceof:

class EsNumerico {
  static [Symbol.hasInstance](valor) {
    return typeof valor === 'number' || (
      typeof valor === 'string' && !isNaN(Number(valor)) && valor.trim() !== ''
    );
  }
}

console.log(42 instanceof EsNumerico);      // true
console.log('3.14' instanceof EsNumerico);  // true
console.log('hola' instanceof EsNumerico);  // false
console.log(null instanceof EsNumerico);    // false

Otros well-known symbols útiles

Hay otros Symbols predefinidos que permiten personalizar distintos aspectos del motor:

// Symbol.toStringTag: personaliza Object.prototype.toString
class ColeccionPersonalizada {
  get [Symbol.toStringTag]() {
    return 'ColeccionPersonalizada';
  }
}
const col = new ColeccionPersonalizada();
console.log(Object.prototype.toString.call(col)); // "[object ColeccionPersonalizada]"

// Symbol.isConcatSpreadable: controla cómo se comporta en Array.prototype.concat
const puntosExtra = [4, 5, 6];
puntosExtra[Symbol.isConcatSpreadable] = false;
console.log([1, 2, 3].concat(puntosExtra)); // [1, 2, 3, [4, 5, 6]]

// Symbol.species: controla qué constructor se usa en métodos que devuelven nuevas instancias
class MiArray extends Array {
  static get [Symbol.species]() { return Array; }
}
const m = new MiArray(1, 2, 3);
console.log(m.map(x => x * 2) instanceof MiArray); // false — devuelve Array normal

Los Symbols son la solución idiomática de JavaScript para añadir metadatos o comportamientos personalizados a objetos sin riesgo de colisión con código existente o futuro. Los well-known symbols, en particular, son la API que el motor usa para exponer puntos de extensión del lenguaje de forma formal y predecible.

COMPARTE ESTE ARTÍCULO

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