Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.
Los símbolos son un elemento nuevo en ECMAScript que ofrece la posibilidad de crear valores únicos. Los símbolos son objetos de un tipo básico nuevo llamado symbol. Esos objetos son inmutables (el valor asociado a un símbolo no puede cambiarse después de su creación) y únicos (no hay dos símbolos con el mismo valor).
Los símbolos se crean usando la función Symbol:
var s1 = Symbol("foo");
A la función Symbol se le pasa una cadena (no se admite llamar a Symbol sin parámetros) que sirve como identificador del símbolo. Pero aunque le pasemos la misma cadena a dos símbolos, cada uno de ellos es distinto para ECMAScript. Por ejemplo, el siguiente código crea dos símbolos distintos:
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2; // false
s1 == s2; // false
Tampoco hay conversión alguna entre el símbolo y la cadena que se haya usado para crearlo:
var s1 = Symbol("foo"); s1 === "foo"; // false s1 == "foo"; // false
A partir de la cadena que se haya usado como identificación del símbolo (foo en nuestros ejemplos) no hay manera de poder obtener el símbolo asociado a ella.
Fíjate en que la función Symbol no es una función constructora. Usar new Symbol genera un error.
Los símbolos son un tipo nuevo, es decir usar typeof sobre una variable de tipo símbolo no devolverá “object”, sino que devolverá “symbol”.
Los símbolos en general no se convierten de forma automática a ningún tipo, e intentar concatenar un símbolo con una cadena da error:
var s1 = Symbol("foo"); var str = s1 + "bar"; // TypeError
Un símbolo básicamente solo es igual a sí mismo o bien a un objeto creado con Object(s) siendo s el propio símbolo:
var s1 = Symbol("foo"); var obj = Object(s1); typeof(s1); // "symbol" typeof(obj); // "object" s1 == obj; // true s1 === obj; // false
Para convertir un símbolo en una cadena puede usarse String(s) (siendo s el símbolo), pero esto no devuelve la cadena que se haya usado como identificador, sino que devuelve una representación en formato cadena del símbolo:
var s1 = Symbol("foo"); var str = String(s1); //"Symbol(foo)" str == s1; // false str === s1; // false str == "foo"; // false str === "foo"; // false
Una de las cosas que mucha gente achaca a JavaScript es la imposibilidad de declarar métodos o propiedades privadas dentro de un objeto. Es cierto que el lenguaje no tiene ninguna palabra clave para declarar métodos o propiedades privadas, pero eso no implica que no se puedan crear: solo significa que no es tan directo como en otros lenguajes. Sí que es cierto que la notación de objeto no permite hacerlo, por lo que el mecanismo siempre pasa por crear el objeto a través de una función (sea o no constructora).
Este patrón se usa en JavaScript para crear un singleton, es decir un objeto del que existe una sola instancia. Por supuesto nada impide que este objeto sea una función (en JavaScript las funciones son un tipo particular de objetos) que permita crear otros objetos, por lo que puedes usar este patrón para exponer una factoría.
Factoría: Un patrón de orientación a objetos. Una factoría es un objeto (o una función) cuya funcionalidad es la de crear otros objetos. Se suele usar cuando la creación del objeto no es trivial y/o bien requiere personalizaciones posteriores a la creación del objeto pero necesarias antes de poder usarlo.
El revealing module pattern consiste en una función anónima autoejecutable que termina devolviendo un objeto. Este objeto devuelto es el que actúa como singleton. Para “exponer” el singleton y que sea accesible se puede: o bien asignar a una variable global, o bien guardarlo dentro de algún contexto (usualmente el global). Todo lo que se declara dentro de la función anónima autoejecutable es privado y solo lo que se devuelve al final es accesible:
var Modulo = (function() { var p1 = function() { console.log("En p1"); p2();} var p2 = function() { console.log("En p2");} return { pub1: p1 }; })(); Modulo.p1(); // Error p1 is not a function Modulo.p2(); // Error p2 is not a funcion Modulo.pub1(); // Ejecuta pub1 (que es realmente p1)
Observa como todas las variables declaradas dentro de la función anónima no son accesibles desde fuera: las funciones p1 y p2 no se pueden llamar desde el exterior. Solo se puede acceder al objeto que se devuelve en el return. Si queremos hacer que alguna función declarada en el módulo, sea accesible debemos devolverla como una propiedad del objeto (es el caso de pub que simplemente nos permite acceder a la propia función privada p1).
El revealing module pattern se basa simplemente en el hecho de que todas las variables locales no son accesibles desde el exterior: p1 y p2 son variables locales de la función anónima autoejecutable.
Observa que la variable Modulo no contiene la función anónima, sino el resultado de invocar a la función anónima (es decir el objeto que devolvemos). Observa que la función la declaramos con:
(function() {...})();
Ese código no es (solo) la declaración de una función anónima: es la declaración de una función anónima y su invocación (observa los paréntesis finales). A esta construcción (declarar y ejecutar ipso-facto una función anónima) la llamamos “función anónima autoejecutable”. Una vez ejecutada ya no se puede volver ejecutar, dado que la función era anónima y no la hemos guardado en ninguna variable.
El revealing module pattern es pues una de las opciones que podemos usar para tener realmente funciones y métodos privados. El hándicap es que realmente es un singleton (no podemos tener dos instancias de Modulo) Si queremos tener algo más parecido a una “clase” de la cual crear objetos y que tenga métodos o propiedades privadas debemos usar una función constructora.
Usando una función constructora, el tener variables o funciones privadas a nivel de objeto es trivial: basta con no asignarlas a this sino declararlas como locales a la función constructora. Dado que cada creación al objeto implica una llamada a la función constructora esas variables locales existen por cada objeto:
var Foo = function() { var data = 42; this.value = function() { if (arguments.length == 0) { return data; } else { data = arguments[0]; } } this.inc = function() {data++} } var f1 = new Foo(); f1.value(); // 42 var f2 = new Foo(); f1.value(100); f1.value(); // 100 f2.value(); // 42 f1.data; // undefined f2.data; // undefined
En este caso la variable data es una variable privada de cada objeto construido mediante la función constructora Foo. Solo lo que se asigna a this forma parte real del objeto y por lo tanto es accesible.
Igual te estás preguntando cómo es posible que la variable data no sea destruida al finalizar la ejecución de la función Foo. La pregunta tiene toda la lógica del mundo, ya que la variable data es una variable local y las variables locales son destruidas al finalizar la ejecución de la función. La razón por la que no es destruida es porque es utilizada (accedida) desde funciones que tienen más ámbito de vida (en este caso desde funciones que se asignan a this y por lo tanto su ámbito de vida es el del objeto). Cuando una función declarada dentro de otra función (p. ej. la función value declarada dentro de la función Foo) accede a una variable local de la función contenedora, decimos que la función contenida “captura la variable” y que ejerce de closure.
El Revealing Module Pattern es uno de los patrones que más se utilizan en JavaScript para tener variables y funciones de visibilidad privada. Básicamente, dicho patrón, consiste en una función anónima autoejecutable:
(function() { })();
Todo lo que definamos dentro de la anterior función, serán variables y funciones privadas.
var miModulo = (function() { var value = 42; var foo = function(i) { return i + value * bar(); } var bar = function() { return 10;} return { // El primer ''foo'' es el nombre público y el segundo es el privado foo: foo } })();
La función foo es privada, pero la exponemos al devolverla con el objeto en el último return.
Al ejecutar el código anterior:
miModulo; // -> Object { foo: foo() } miModulo.pFoo(10); // -> 62 // Comprobamos que no podemos acceder a variables y funciones privadas: miModulo.value; // -> undefined miModulo.bar(); // -> TypeError: miModulo.bar is not a function miModulo.foo(); // -> TypeError: miModulo.foo is not a function
Los símbolos ofrecen un nuevo mecanismo para declarar variables privadas en un objeto. Para ello podemos usar un símbolo como nombre de propiedad de un objeto. Eso garantiza que solo quien tenga acceso al símbolo original podrá acceder a dicha propiedad, ya que no hay otro modo de obtener el nombre de la propiedad.
Las propiedades cuyo nombre es un símbolo no aparecen ni al usar Object.getOwnProperyNames ni Object.keys.
var obj={ v1: 42, [Symbol("v2")]: 100 };
Dado el siguiente código, si miramos las propiedades que tiene obj tan solo obtendremos la propiedad v1. La otra propiedad (cuyo nombre es un símbolo) no la vemos de ninguna manera:
Se puede ver que la propiedad Symbol("v2") no se obtiene de ninguna manera. Para poder acceder a esta propiedad necesitamos el símbolo original. Ahora bien, ¿cuál es el valor de result en este código?
var obj={ v1: 42, [Symbol("v2")]: 100 }; var result = obj[Symbol("v2")];
Al finalizar este código el valor de result es undefined. La razón es que no estamos usando el mismo símbolo para acceder a la propiedad. Recuerda que dos símbolos son distintos a pesar de que los creemos con el mismo identificador.
Por la misma razón el siguiente código no modifica el valor de la propiedad de 100 a 200, sino que crea otra propiedad con el valor 200 (distinta de la propiedad con el valor de 100).
var obj={ v1: 42, [Symbol("v2")]: 100 }; obj[Symbol("v2")] = 200;
Para acceder a la propiedad cuyo nombre es un símbolo necesitamos el símbolo original:
var s = Symbol("v2"); var obj={ v1: 42, [s]: 100 }; var result = obj[s]; // 100
Observa que colocamos la variable símbolo entre corchetes para acceder a la propiedad. Si no usásemos los corchetes simplemente estaríamos creando (o accediendo a) una propiedad llamada “s”
Un símbolo nos permite obtener un nombre único.
var obj = { [Symbol("v")]: 42, v: 60 }
obj; // -> Object { v: 60}
No nos muestra la otra propiedad, la creada con el símbolo. Solo podemos acceder a ella utilizando el símbolo:
obj[Symbol("v")]; // -> undefined Symbol("v") === Symbol("v"); // false
Así que tendríamos que haber guardado el símbolo:
var s = Symbol("v"); var obj = { [s]: 42, v: 60 }
Tras ejecutarlo:
obj[s]; // -> 42
Podríamos ver todos los símbolos usados como propiedad en un objeto con Object.getOwnPropertySymbols(obj):
var sy = Object.getOwnPropertySymbols(obj); sy; // -> Array[ Symbol(v) ] obj[sy[0]]; // -> 42
Los símbolos nos dan otro mecanismo para definir propiedades privadas, pero esto no impide que alguien utlizando Object.getOwnPropertySymbols(obj) pueda obtener los símbolos que se han utilizado para esas propiedades y acceder directamente a ellas.
Si quisiéramos ocultar realmente una propiedad, no podemos usar símbolos. Deberíamos usar el revealing module pattern o utilizar el concepto de función constructora.
var handle = function(name) { var _name = name; var _status = "C"; // 'C' cerrado return { // getter para devolver la propiedad privada get name() { return _name; }, get status() { return _status; }, open() { if (_status == "C") { console.log("Abriendo " + _name); _status = "A"; } else { console.log("Ya está abierto"); } } } }
Tras ejecutarlo:
var h = handle("test"); h.name; // -> 'test' h.status; // -> 'C' h.open(); // -> Abriendo test h.open(); // -> Ya está abierto
Para evitar colisiones con otros posibles valores, usaremos símbolos porque de esa manera tendremos valores únicos para nuestras propiedades y aumentamos la seguridad de nuestro código:
var handle_status = { Abierto: Symbol("A"), Cerrado: Symbol("C") } var handle = function(name) { var _name = name; var _status = handle_status.Cerrado; return { // getter para devolver la propiedad privada get name() { return _name; }, get status() { return _status; }, open() { if (_status === handle_status.Cerrado;) { console.log("Abriendo " + _name); _status = handle_status.Abierto; } else { console.log("Ya está abierto"); } } } }
Tras ejecutarlo:
var h = handle("edu"); h; // -> Object { name: Getter, status: Getter, open: open() } h.name; // -> 'edu'h h.status; // -> Symbol(C) h.open(); // -> Abriendo edu h.open(); // -> Ya está abierto h.status === handle_status.Abierto; // -> true
Queremos hacer un objeto que sirva para mostrar información de otros objetos:
var Dumper = function() { this.dumpObject = function(obj) { console.log("Dumping object"); if (typeof(obj["dump"]) == "function") { obj["dump"](); } else { for (var k in obj) { console.log(k, obj[k]); } } } }
Creamos un objeto:
var a = {v: 42, ov: 69, foo() {} }; var b = {v: 42, ov: 69, foo() {}, dump() { console.log("dumping b"); } }; var myDumper = new Dumper(); myDumper.dumpObject(a); myDumper.dumpObject(b);
Para evitar colisiones con otros métodos dump:
var Dumper = function() { this.dumpObject = function(obj) { console.log("Dumping object"); if (typeof(obj[Dumper.dump]) == "function") { obj[Dumper.dump](); } else { for (var k in obj) { console.log(k, obj[k]); } } } } Dumper.dump = Symbol("dump"); var a = {v: 42, ov: 69, foo() {}, [Dumper.dump]() { console.log("dumping a"); } }; var myDumper = new Dumper(); myDumper.dumpObject(a);
Ahora este código no tendrá colisiones con ningún otro objeto.
A pesar de que el objetivo básico de los símbolos es proporcionar nombres únicos y evitar las colisiones, de forma que solo quien tenga acceso al símbolo original podrá reproducir este nombre, ECMAScript 2015 da la posibilidad de crear símbolos globales. Un símbolo global es un símbolo pero que cualquiera que conozca la cadena identificativa de este símbolo puede recrear:
var s1 = Symbol.for("s"); var s2 = Symbol.for("s"); s1 === s2; // true s1 == s2; // true var s3 = Symbol("s"); s1 === s3; // false var s4 = Symbol.for("s4"); s1 === s4; // false
Los símbolos globales se crean con Symbol.for() y se le pasa la cadena a usar como identificador. Cualquiera que conozca esta cadena puede usar de nuevo Symbol.for() para obtener el símbolo. En el código anterior se ve como s1 y s2 son iguales (ambas están creadas con Symbol.for y la misma cadena identificativa). Por otro lado s3 no es igual a s1 (o s2) ya que, a pesar de usar la misma cadena identificativa, no se trata de un símbolo global. Por último, s4 también es distinto de s1 y s2 ya que, a pesar de ser un símbolo global, se ha usado otra cadena identificativa para crearlo.
Los símbolos globales permiten seguir usando símbolos para métodos o valores concretos, pero evitan el tener que exponer este símbolo para quien necesite usarlo.
En el vídeo anterior, teníamos que exponer el símbolo a través de la variable Dumper.dump, para que los usuarios de nuestro objeto Dumper pudiesen usar este símbolo para crear el método específico. Usando un símbolo global, no es necesario exponer el símbolo (basta con que los usuarios sepan el identificador y luego usen Symbol.for). La contrapartida es que podemos volver a tener colisiones si dos objetos distintos esperan métodos (o propiedades) cuyo nombre sea el mismo símbolo global. Pero dado que el identificador es una cadena, la probabilidad de colisiones (si cada librería elige bien sus identificadores) es menor que usando simplemente nombres de métodos (que al tener que cumplir ciertas reglas sintácticas son más limitados).