Índice de contenidos
1. Entorno
- Hardware: Portátil MacBook Pro 15 pulgadas (2.5 GHz Intel i7, 32GB 2400 Mhz
DDR4, 500GB Flash Storage). - Sistema Operativo: MacOs Catalina 10.15.3
- Visual Studio Code con la extensión de Quokka.js instalada.
2. Qué son los símbolos
Durante estos tiempos un poco raros, estoy aprovechando para ponerme al día con
otros lenguajes de programación con los que no trabajo normalmente como es el
caso de JavaScript y la especificación EcmaScript 6. Una de las novedades
que más me ha llamado la atención son los símbolos, cuyo uso no es tan claro y
los que somos poco hábiles en esto del desarrollo front o nos dedicamos más al
desarrollo backend con otros lenguajes no entendemos muy bien. Este tutorial
hace un repaso de las principales características de los símbolos y los
distintos usos que podemos darles.
3. Cómo crear símbolos
Para crear símbolos sólo es necesario referenciar con Symbol pero sin utilizar la palabra reservada new ya que nos dará un error TypeError. En la creación se puede indicar un tag a través de un string.
<pre class="lang:js decode:true">const symbol1 = Symbol('symbol1-key');
const symbol2 = Symbol('symbol1-key');
const symbol3 = new Symbol('typeError'); // Error: Symbol is not a constructor
A primera vista, diríamos que symbol1 y symbol2 son iguales. Sim embargo, si los comparamos:
<pre class="lang:js decode:true">const bool = symbol1 === symbol2;
console.log(bool); // false
Esto es debido a que el primer parámetro que podemos incluir en el constructor es meramente un tag para etiquetar al símbolo y util en debug. Recordemos que en la definición decíamos que un símbolo es una especie de identificador único (p.e. un UUID) y por tanto dos símbolos siempre van a ser diferentes.
<pre class="lang:js decode:true">const symbol1 = Symbol('symbol1-key');
console.log(symbol1.toString()); // Symbol(symbol1-key)
Pueden crearse símbolos usando la misma etiqueta en el constructor, pero se recomienda no hacerlo para evitar confusiones.
4. Aplicaciones de los símbolos
Uno de los principales usos de los símbolos es referenciar valores internos de las clases sin tener que conocer realmente dicho valor. Por ejemplo, tenemos una clase transporte para definir tipo de transporte camión, furgoneta o barco con distintos kilos de capacidad. Podemos utilizar símbolos para definir los tipos de transporte y utilizarlos para crear instancias de Transport:
<pre class="lang:js decode:true">var VAN = Symbol('van');
var TRUCK = Symbol('truck');
var FERRY = Symbol('ferry');
class Transport {
constructor(type) {
switch(type) {
case VAN:
this.capacity = 300; //Furgoneta 300 kilos max
this.name = "Van";
break;
case TRUCK:
this.capacity = 1200; //Camión 1200 kilos max
this.name = "Truck";
break;
case FERRY:
this.capacity = 5000; //Furgoneta 5000 kilos max
this.name = "Ferry";
break;
default:
throw new TypeError('Invalid transport type');
}
}
toString() {
return `Transport type ${this.name} with capacity ${this.capacity}`;
}
}
De esta manera es el constructor el responsable de inicializar la capacidad según el medio de transporte estableciendo unos valores concretos.
<pre class="lang:js decode:true">const van = new Transport(VAN);
console.log(van.toString()); // Transport type Van with capacity 300
const truck = new Transport(TRUCK);
console.log(truck.toString()); // Transport type Truck with capacity 1200
const ferry = new Transport(FERRY);
console.log(ferry.toString()); // Transport type Ferry with capacity 5000
const other = new Transport("other"); // Invalid transport type
console.log(other.toString());
Otra de las aplicaciones de los símbolos es la de poder ocultar propiedades en nuestras clases, evitando revelar desde fuera sus valores. Si por ejemplo tenemos una clase persona en la que el id y password asociados no queremos publicar hacia fuera:
<pre class="lang:js decode:true"> const PASSWORD = Symbol("password");
const ID = Symbol("id");
class Person {
constructor(name, lastName, email) {
this[ID] = 1;
this.name = name;
this.id = 4;
this.lastName = lastName;
this.email = email;
this[PASSWORD] = "generatedPassword";
}
getRelatedInfo() {
console.log(this[ID]); // 1
console.log(this.id); // 4
return {
detail: "more_details",
};
}
}
var p1 = new Person("sara", "martinez", "email@mail.com");
console.log(Object.getOwnPropertyNames(p1)); // [ 'name', 'id', 'lastName', 'email' ]
console.log(JSON.stringify(p1)); // {"name":"sara",'id':4, "lastName":"martinez","email":"email@mail.com"}
console.log(p1.getRelatedInfo()); // { detail: 'more_details' }
console.log(Object.getOwnPropertySymbols(p1)); // [ Symbol(id), Symbol(password) ]
console.log(Reflect.ownKeys(p1)); // [ 'name', 'id', 'lastName', 'email', Symbol(id), Symbol(password) ]
Si os fijáis, tenemos una propiedad de clase id y otra referenciada a través del símbolo, cada una con su valor. Esto es útil cuando queremos añadir más propiedades asegurándonos de no sobreescribir propiedades ya definidas en la clase ni sus valores.
Sin embargo, estas propiedades no son del todo privadas, ya que podemos recuperar los símbolos a través de los métodos *Object.*getOwnPropertyNames() y Reflect.ownKeys():
<pre class="lang:js decode:true ">console.log(Object.getOwnPropertySymbols(p1)); // [ Symbol(id), Symbol(password) ]
console.log(Reflect.ownKeys(p1)); // [ 'name', 'lastName', 'email', Symbol(id), Symbol(password) ]
Dentro de la especificación ES6, existen una serie de símbolos predefinidos, llamados Well-known symbols, que podemos utilizar para añadir funcionalidad a nuestras clases y tipos. Uno de ellos es Symbol.Iterator. Por ejemplo, las clases Map, String o Set lo utilizan para poder recorrer así sus elementos.
Podemos implementar nuestros propios iterators. Vamos a crear una clase en la que podamos recorrer los dígitos de un valor pasado como parámetro:
<pre class="lang:js decode:true">class OwnIterator {
constructor(number) {
this.number = number;
}
[Symbol.iterator]() {
let index = 0;
let numbersArray = this.number.toString().split("");
return {
next() {
return {
value: Number(numbersArray[index]),
done: index++ >= numbersArray.length,
};
},
};
}
};
const obj = new OwnIterator(53820391);
for (let a of obj) {
a; // 5,3,8,2,0,3,9,1
}
Por último, existe también una manera de definir símbolos que podemos utilizar a nivel global a través de un Global Registry y poder así referenciarlos desde distintos módulos y ficheros. Para crear símbolos dentro de este registry debemos usar la palabra reservada for:
<pre class="lang:js decode:true">const symb = Symbol.for('symbol');
const symb2 = Symbol.for('symbol');
console.log(symb === symb2); // true
console.log(Symbol.keyFor(symb)) // symbol
Además, como vemos en este caso, symb y symb2 son iguales porque están refenciando a la misma clave.
5. Conclusiones
Como hemos visto, esta novedad de ES6 es algo más que introducir un nuevo tipo o implementar iteradores. En muchos cursos de novedades ES6 no se ahonda mucho en sus usos y creo que es muy interesante conocer. Espero que os sirva y que a partir de ahora conozcáis el por qué de su existencia.