26º julio 2020

Tipo Symbol

Por especificación, las claves (Keys) de un objeto deben ser solamente del tipo String o Symbol. Solamente esos dos: String o Symbol.

Hasta ahora sólo hemos aprendido acerca de los Strings, por lo que es momento de conocer las ventajas que Symbol nos puede dar.

Symbols

El valor de “Symbol” representa un identificador único.

Un valor de este tipo puede ser creado usando Symbol():

// id es un nuevo symbol
let id = Symbol();

También le podemos agregar una descripción (también llamada symbol name), que será útil en la depuración de código:

// id es un symbol con la descripción "id"
let id = Symbol("id");

Se garantiza que los símbolos son únicos. Aunque declaremos varios Symbols con la misma descripción, éstos tendrán valores distintos. La descripción es solamente una etiqueta que no afecta nada más.

Por ejemplo, aquí hay dos Symbols con la misma descripción – pero no son iguales:

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

Si estás familiarizado con Ruby u otro lenguaje que también tiene symbols – por favor no te confundas. Los Symbols de Javascript son diferentes.

Symbols no se autoconvierten a String

La mayoría de los valores en JavaScript soportan la conversión implícita a string. Por ejemplo, podemos hacer un ´alert´ con casi cualquier valor y funcionará. Los Symbols son distintos, éstos no se auto-convierten.

Por ejemplo, este alert mostrará un error:

let id = Symbol("id");
alert(id); // TypeError: No puedes convertir un valor Symbol en string

Esa es una “protección del lenguaje” para evitar errores ya que los String y los Symbol son diferentes y no deberían convertirse ocasionalmente uno en otro.

Si realmente queremos mostrar un Symbol, necesitamos llamar el método .toString() de la siguiente manera:

let id = Symbol("id");
alert(id.toString()); // Symbol(id), ahora sí funciona

O se puede utilizar symbol.description para obtener la descripción solamente:

let id = Symbol("id");
alert(id.description); // id

Claves “Ocultas”

Los Symbols nos permiten crear claves “ocultas” en un objeto, a las cuales ninguna otra parte del código puede accesar ni sobrescribir.

Por ejemplo, si queremos guardar un “identificador” para el objeto user, podemos asignar un symbol como clave del objeto:

let user = { // pertenece a otro código
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // podemos accesar a la información utilizando  el symbol como nombre de clave

¿Cuál es la ventaja de usar Symbol("id") y no un string "id"?

Vamos a profundizar en el ejemplo para que sea más claro.

Imagina que otro script quiere tener la clave “id” dentro de user para sus propios fines. Puede ser otra librería de JavaScript, por lo cual ninguno de los scripts saben de su coexistencia.

Y entonces ese script puede crear su propio Symbol("id"), como este:

// ...
let id = Symbol("id");

user[id] = "Su id";

No habrá conflicto porque los Symbols siempre son diferentes, incluso si tienen el mismo nombre.

Ahora ten en cuenta que si utilizamos un string "id" en lugar de un Symbol para el mismo propósito, entonces SÍ habría un conflicto:

let user = { name: "John" };

// Nuestro script usa la clave "id"
user.id = "Nuestro valor id";

// ...Otro script también quiere usar "id"  ...

user.id = "Su valor de id"
// Boom! sobreescrito para otro script!

Symbols en objetos literales

Si queremos usar un Symbol en un objeto literal, debemos usar llaves.

Como se muestra a continuación:

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // no "id": 123
};

Se hace así porque necesitamos que el valor de la variable id sea la clave, no el string “id”.

Los Symbols son omitidos en for…in

Las claves de Symbol no participan dentro de los ciclos for..in.

Por ejemplo:

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // nombre, edad (no symbols)

// el acceso directo a la clave de symbol funciona
alert( "Direct: " + user[id] );

Esto forma parte del concepto general de “ocultamiento”. Si otro script o si otra librería itera el objeto este no accesará a la clave de Symbol.

En contraste, Object.assign copia las claves tanto del string como las del symbol:

let id = Symbol("id");
let user = {
  [id]: 123
};

let clone = Object.assign({}, user);

alert( clone[id] ); // 123

No hay paradoja aquí, es así por diseño. La idea es que cuando clonamos un objeto o cuando fusionamos objetos, generalmente queremos que se copien todas las claves (incluidos los Symbol como id).

Symbols Globales

Como hemos visto, normalmente todos los Symbols son diferentes aunque tengan el mismo nombre. Pero algunas veces necesitamos que los symbol con el mismo nombre sean las mismas entidades.

Por ejemplo, distintas partes de nuestra aplicación quieren accesar a symbol "id" queriendo obtener el mismo valor de la clave.

Para lograr esto, existe un global symbol registry. Ahí podemos crear symbols y acceder después a ellos, lo cual nos garantiza que cada vez que se acceda a la clave con el mismo nombre, esta te devuelva exactamente el mismo symbol.

Para crear u accesar a un symbol en el registro global, usa Symbol.for(key).

Esta llamada revisa el registro global, y si existe un symbol descrito como key, lo retornará, de lo contrario creará un nuevo symbol Symbol(key) y lo almacenará en el registro por su key.

Por ejemplo:

// leer desde el registro global
let id = Symbol.for("id"); // si el símbolo no existe, se crea

// léelo nuevamente (tal vez de otra parte del código)
let idAgain = Symbol.for("id");

// el mismo symbol
alert( id === idAgain ); // true

Los Symbols dentro de este registro son llamados global symbols y están disponibles y al alcance de todo el código en la aplicación.

Eso suena a Ruby

En algunos lenguajes de programación como Ruby, hay un solo Symbol por cada nombre.

En Javascript, como podemos ver, existen los global symbols.

Symbol.keyFor

Para los global symbols, no solo Symbol.for(key) devuelve un symbol por su nombre, sino que existe una llamada inversa: Symbol.keyFor(sym) que hace lo contrario: devuelve el nombre de un global symbol.

Por ejemplo:

// tomar symbol por nombre
let sym = Symbol.for("nombre");
let sym2 = Symbol.for("id");

// tomar name por symbol
alert( Symbol.keyFor(sym) ); // nombre
alert( Symbol.keyFor(sym2) ); // id

El Symbol.keyFor utiliza internamente el registro “global symbol registry” para buscar la clave del symbol, por lo tanto, no funciona para los symbol que no están dentro del registro. Si el symbol no es global, no será capaz de encontrarlo y por lo tanto devolverá undefined.

Dicho esto, todo symbol tiene description de clave.

Por ejemplo:

let globalSymbol = Symbol.for("nombre");
let localSymbol = Symbol("nombre");

alert( Symbol.keyFor(globalSymbol) ); // nombre, global symbol
alert( Symbol.keyFor(localSymbol) ); // undefined, no global

alert( localSymbol.description ); // nombre

System symbols

Existen varios symbols del sistema que JavaScript utiliza internamente, y que podemos usar para ajustar varios aspectos de nuestros objetos.

Se encuentran listados en Well-known symbols :

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • …y así.

Por ejemplo, Symbol.toPrimitive nos permite describir el objeto para su conversión primitiva. Más adelante veremos su uso.

Otros symbols también te serán más familiares cuando estudiemos las características correspondientes.

Resumen

Symbol es un tipo de dato primitivo para identificadores únicos.

Symbols son creados al llamar Symbol() con una descripción opcional.

Symbols son siempre valores distintos aunque tengan el mismo nombre. Si queremos que symbols con el mismo nombre tengan el mismo valor, entonces debemos guardarlos en el registro global: Symbol.for(key) retornará un symbol (en caso de no existir, lo creará) con el key como su nombre. Múltiples llamadas de Symbol.for retornarán siempre el mismo symbol.

Symbols se utilizan principalmente en dos casos:

  1. Claves(keys) “Ocultas” dentro de un objeto. Si queremos agregar una clave a un objeto que “pertenezca” a otro script u otra librería, podemos crear un symbol y usarlo como clave. Una clave de symbol no aparecerá en los ciclos for..in,por lo que no aparecerá listada. Tampoco podrá ser accesada directamente por otro script porque este no tendrá nuestro symbol y no podrá intervenir en sus acciones.

    Podemos “ocultar” ciertos valores dentro de un objeto que solo estarán disponibles dentro de ese script usando las claves de symbol.

  2. Existen diversos symbols del sistema que utiliza Javascript, a los cuales podemos accesar por medio de Symbol.*. Podemos usarlos para alterar algunos comportamientos. Por ejemplo, más adelante en el tutorial, usaremos Symbol.iterator para iterables, Symbol.toPrimitive para configurar object-to-primitive conversion.

Técnicamente, los symbols no están 100% ocultos. Existe un método incorporado Object.getOwnPropertySymbols(obj) que nos permite obtener todos los symbols. También existe un método llamado Reflect.ownKeys(obj) que devuelve todas las claves de un objeto, incluyendo las que son de tipo symbol. Por lo tanto, no están realmente ocultos, aunque la mayoría de las librerías, los métodos incorporados y las construcciones de sintaxis se adhieren a un acuerdo común de que sí lo están. Y el que explícitamente llama a los métodos antes mencionados probablemente entiende bien lo que está haciendo.

Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)