Symbol en JavaScript: identificadores únicos y well-known symbols

Symbol es un tipo primitivo de JavaScript introducido en ES6. Cada valor Symbol es único e irrepetible, incluso si dos símbolos se crean con la misma descripción. Su principal uso es crear claves de propiedad que no colisionen con otras propiedades, sean del código de usuario o de la plataforma.

Crear y usar Symbol

Un símbolo se crea llamando a Symbol(descripción). La descripción es opcional y solo sirve para depuración. Dos llamadas con la misma descripción producen símbolos distintos:

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

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

// Uso como clave de propiedad
const ID = Symbol('id');
const SECRETO = Symbol('secreto');

const usuario = {
  nombre: 'Ana',
  [ID]: 42,           // clave simbólica
  [SECRETO]: 'token_privado'
};

console.log(usuario[ID]);       // 42
console.log(usuario.nombre);    // 'Ana'

// Las claves simbólicas no aparecen en enumeración normal
console.log(Object.keys(usuario));           // ['nombre']
console.log(JSON.stringify(usuario));        // {"nombre":"Ana"}
console.log(Object.getOwnPropertySymbols(usuario)); // [Symbol(id), Symbol(secreto)]

Symbol.for(): registro global de símbolos

Symbol.for(clave) busca en un registro global. Si ya existe un símbolo con esa clave, lo devuelve; si no, lo crea. Permite compartir el mismo símbolo entre módulos distintos:

// En módulo A:
const ID_A = Symbol.for('app:id');

// En módulo B (otra parte del código):
const ID_B = Symbol.for('app:id');

console.log(ID_A === ID_B);  // true — mismo símbolo del registro global

// Symbol() siempre crea uno nuevo
const local1 = Symbol('local');
const local2 = Symbol('local');
console.log(local1 === local2);  // false

// Symbol.keyFor() obtiene la clave del registro
console.log(Symbol.keyFor(ID_A));   // 'app:id'
console.log(Symbol.keyFor(local1)); // undefined (no está en el registro)

Well-known symbols: personalizar comportamientos del lenguaje

JavaScript usa un conjunto de símbolos predefinidos (well-known symbols) para definir comportamientos que pueden personalizarse en tus objetos. Son propiedades estáticas de Symbol:

// Symbol.iterator: hacer un objeto iterable con for...of
class Rango {
  constructor(inicio, fin) {
    this.inicio = inicio;
    this.fin = fin;
  }

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

const rango = new Rango(1, 5);
console.log([...rango]);        // [1, 2, 3, 4, 5]
for (const n of rango) console.log(n);  // 1 2 3 4 5

Symbol.toPrimitive: controlar la conversión de tipos

Symbol.toPrimitive permite definir cómo un objeto se convierte a primitivo en distintos contextos:

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

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number': return this.cantidad;
      case 'string': return `${this.cantidad} ${this.moneda}`;
      default: return this.cantidad;  // 'default' hint
    }
  }
}

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

console.log(+precio);          // 99.99 (hint: 'number')
console.log(`${precio}`);      // '99.99 EUR' (hint: 'string')
console.log(precio + 0.01);    // 100 (hint: 'default')
console.log(precio > 50);      // true

Symbol.hasInstance y Symbol.toStringTag

// Symbol.hasInstance: personalizar instanceof
class EsArray {
  static [Symbol.hasInstance](instancia) {
    return Array.isArray(instancia);
  }
}

console.log([] instanceof EsArray);     // true
console.log({} instanceof EsArray);     // false

// Symbol.toStringTag: personalizar Object.prototype.toString
class MiColeccion {
  get [Symbol.toStringTag]() {
    return 'MiColeccion';
  }
}

const col = new MiColeccion();
console.log(Object.prototype.toString.call(col));  // '[object MiColeccion]'
console.log(col.toString());  // '[object MiColeccion]'

// Comparación con tipos nativos:
Object.prototype.toString.call([]);      // '[object Array]'
Object.prototype.toString.call(null);   // '[object Null]'
Object.prototype.toString.call(/re/);   // '[object RegExp]'

Los símbolos se usan principalmente en librerías y código de infraestructura donde necesitas añadir propiedades a objetos ajenos sin riesgo de colisión, o cuando quieres personalizar el comportamiento de tus objetos al interactuar con las operaciones del lenguaje. En código de aplicación, el uso más frecuente es Symbol.iterator para crear colecciones propias y Symbol.for() para compartir identificadores entre módulos.

COMPARTE ESTE ARTÍCULO

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