En algunas situaciones puede que en nuestro código nos interese utilizar variables o propiedades para identificar ciertos elementos. Esos elementos serán localizados por identificadores únicos, que generalmente suele ser un código
¿Qué es un Symbol?
En Javascript, existe un tipo de dato llamado Symbol
que no suele ser muy conocido. Se trata de una forma alternativa de crear identificadores únicos que ofrece algunas ventajas y garantías sobre usar nombres o números.
Antes de ver un ejemplo práctico, vamos a entender como funcionan los Symbol
, ya que su funcionamiento es realmente simple:
const id = Symbol("id"); // Identificador de "id"
const unique = Symbol("unique"); // Identificador de "unique"
Symbol("unique") === Symbol("unique"); // false
Declarando un Symbol()
y pasándole por parámetro un
Observa que en la última línea, a pesar de crear dos símbolos con el mismo
Si lo que queremos es comprobar si su parámetro es el mismo, podemos acceder a su descripción:
const u1 = Symbol("unique");
const u2 = Symbol("unique");
u1 === u2; // false (son símbolos diferentes)
u1.description === u2.description; // true (se crearon con el mismo texto)
Veamos ahora, donde puede resultar interesante utilizar estos símbolos.
Crear identificadores únicos
Vamos a crear un ejemplo de un videojuego. De momento, no vamos a utilizar
const enemies = [
{ id: "SKELETON", name: "Esqueleto" },
{ id: "SPECTRE", name: "Espectro" },
{ id: "GHOST", name: "Fantasma" }
];
const addEnemy = (id, name) => {
enemies.push({ id, name });
}
// Añadimos nuevo esqueleto a la lista de enemigos
addEnemy("SKELETON", "Esqueleto resplandeciente");
const findEnemyById = (id) => {
return enemies.find(enemy => enemy.id === id);
}
findEnemyById("SKELETON");
// Devuelve { id: "SKELETON", name: "Esqueleto" }
Observa que utilizando GLEAMING_SKELETON
(caso ideal) ha establecido SKELETON
(que es el mismo identificador que el primero que ya existía).
Por lo tanto, si más tarde buscamos el esqueleto resplandeciente por su identificador, nos devolverá el primero que encuentre, es decir, el SKELETON
original (y no el último que hemos añadido).
Veamos ahora esta aproximación utilizando Símbolos, que recordemos que son únicos:
const SKELETON = Symbol("SKELETON");
const SPECTRE = Symbol("SPECTRE");
const GHOST = Symbol("GHOST");
const enemies = [
{ id: SKELETON, name: "Esqueleto" },
{ id: SPECTRE, name: "Espectro" },
{ id: GHOST, name: "Fantasma" }
];
// Enemies methods
const addEnemy = (id, name) => enemies.push({ id, name });
const findEnemyById = (id) => enemies.find(enemy => enemy.id === id);
// Añadimos nuevo esqueleto a la lista de enemigos
const GLEAMING_SKELETON = Symbol("SKELETON");
addEnemy(GLEAMING_SKELETON, "Esqueleto resplandeciente");
findEnemyById(GLEAMING_SKELETON);
// Devuelve: { id: Symbol(SKELETON), name: "Esqueleto resplandeciente" }
findEnemyById(SKELETON);
// Devuelve: { id: Symbol(SKELETON), name: "Esqueleto" }
Observa que al nuevo elemento le hemos asignado un símbolo que tiene SKELETON
como descripción, igual que el esqueleto original. Sin embargo, como los símbolos son únicos, se consideran diferentes. Recuerda que simplemente los estamos metiendo en constantes (o en un objeto o una estructura más organizada) para mejorar su semántica y organización.
Crear símbolos globales
Los símbolos anteriores son locales. Es decir, sólo existen dentro de un ámbito concreto. Pero también podemos crear símbolos globales para que existan en un ámbito global de nuestra aplicación, incluso fuera de los módulos de Javascript.
const u1 = Symbol("unique");
const u2 = Symbol("unique");
u1 === u2; // false
const u1 = Symbol.for("unique"); // Símbolo global compartido
const u2 = Symbol.for("unique"); // Símbolo global compartido
u1 === u2; // true
Por otro lado, también podemos usar Symbol.keyFor()
para pasar un símbolo global y obtener el
const local = Symbol("unique");
const global = Symbol.for("unique");
Symbol.keyFor(global); // "unique"
Symbol.keyFor(local); // undefined
Símbolos conocidos
En Javascript, internamente, se utilizan símbolos para el funcionamiento de ciertas características del lenguaje. Por ejemplo, puedes utilizar símbolos concretos para definir iteradores o para devolver valores primitivos, entre muchos otros.
Veamos algunos ejemplos, como por ejemplo, Symbol.iterator
o Symbol.toPrimitive
.
Símbolo Symbol.iterator
El símbolo Symbol.iterator
se utiliza para definir como iterar un objeto. Observa el siguiente fragmento de código, donde hemos creado un objeto counter
que tiene las propiedades start
y end
. La idea es que ese objeto pueda recorrerse entre esos dos elementos.
Observa también que tiene una una función que utiliza el símbolo Symbol.iterator
para definir como debe iterarse el objeto:
const counter = {
start: 1,
end: 10,
[Symbol.iterator]() {
let current = this.start;
const end = this.end + 1;
return {
next() {
return {
value: current++,
done: current > end
}
}
}
}
}
Ahora, al tener definido ese métod next()
, podemos iterar sobre el objeto counter
utilizando un bucle for...of
o incluso desestructurando:
// Muestra números del 1 al 10
for (const number of counter) {
console.log(number);
}
// Devuelve [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const numbers = [...counter];
De la misma forma que tenemos Symbol.iterator
, podemos utilizar Symbol.asyncIterator
para definir un iterador asíncrono que utiliza operaciones que son propiamente asíncronas.
Si no entiendes muy bien esta parte, algo absolutamente normal, te recomiendo echar un vistazo a la sección de clausuras y de funciones generadoras.
Símbolo Symbol.toPrimitive
Otro símbolo conocido es Symbol.toPrimitive
, mediante el cuál podemos definir funciones que se ejecutarán cuando se esté realizando una conversión implícita al tipo de dato en cuestión.
Por ejemplo, observa este fragmento de código donde hemos creado un objeto llamado theAnswer
:
const theAnswer = {
[Symbol.toPrimitive](hint) {
if (hint === "string") {
return "El sentido de la vida, el universo y todo lo demás.";
}
else if (hint === "number") {
return 42;
}
else {
return null;
}
}
}
Al definir el símbolo Symbol.toPrimitive
como función, mediante su parámetro podemos definir la lógica a ejecutar dependiendo del tipo de dato al que se convierta.
Por ejemplo, ahora podemos hacer lo siguiente:
theAnswer // Devuelve { [Symbol.toPrimitive]: f }
String(theAnswer) // Devuelve "El sentido de la vida, el universo y todo lo demás."
Number(theAnswer) // Devuelve 42