Variables privadas en clases ES2015

Una de las novedades de ES6 es la posibilidad de usar clases. Si bien esas clases son realmente sugar syntax sobre el patrón de función constructora, tienen alguna característica que simplifica el uso de dicho patrón y ofrecen una sintaxis que según como se mire puede ser más cómoda. Pero el estándard de ES2015 no tiene ningún mecanismo para indicar que una variable de una clase sea pública o privada. La visibilidad de variables en JavaScript es un clásico, sobre el que ya he escrito algún post. Pero las técnicas que usábamos hasta ahora para simular la visibilidad privada no aplican cuando usamos la sintaxis de clases… y debemos usar mecanismos alternativos. En este post veremos dos de ellos.

Supongamos que tenemos una clase ES2015 como la siguiente:

  1. class Foo {
  2.     constructor(name) {
  3.         this._name = name;
  4.     }
  5.  
  6.     get name() {
  7.         return _name;
  8.     }
  9. }

El objetivo es que _name sea privado y solo pueda accederse a su valor a través de la propiedad name.

Símbolos al rescate

La primera solución implica el uso de un nuevo concepto de ES2015: los símbolos. Un símbolo permite definir un nombre único desconocido y que tan solo puede ser obtenido a partir de la referencia al símbolo original que ha creado dicho nombre.  El siguiente código explica el uso de símbolos:

  1. var sfoo = Symbol("foo");
  2. var a = {};
  3. a[sfoo] = "sfoo";
  4. console.log(a.sfoo); // undefined
  5. console.log(a[sfoo]);   // sfoo
  6. console.log(a[Symbol("foo")])    // undefined

Podemos asignar una propiedad de un objeto a un símbolo, y entonces requerimos del símbolo para poder obtener el valor de la propiedad. Observa que necesitamos el símbolo original (almacenado en sfoo), crear otro símbolo a partir de la misma cadena, no sirve. Los símbolos son únicos.

Por lo tanto ahora podemos simular variables privadas de la siguiente manera:

  1. const s_name = Symbol("name");
  2. export default class Foo {
  3.     constructor(name) {
  4.         this[s_name] = name;
  5.     }
  6.     get name() {
  7.         return this[s_name];
  8.     }
  9. }

Los objetos de la clase Foo tienen una variable cuyo nombre es el símbolo guardado en s_name. Esa variable “no es realmente pública”, pero no hay manera de encontrar su nombre fuera del módulo que declara la clase (la variable s_name que contiene el símbolo, es privada del módulo por lo que no es accesible desde el exterior). Observa que no basta que alguien mire el código fuente del módulo y se cree otro Symbol(“name”) para acceder a la variable “privada”: los símbolos son únicos, o lo que es lo mismo: Symbol("a") != Symbol("a").

Este método no genera variables totalmente privadas. Las variables que están almacenadas en una propiedad cuyo nombre es un símbolo, no se listan en Object.getOwnPropertyNames y tampoco participan de la iteración de for..in pero se pueden obtener todos los símbolos asociados a un objeto mediante Object.getOwnPropertySymbols, que devuelve un array con todos los símbolos usados como nombre de propiedad del objeto. Basta con usar los símbolos devueltos para acceder a las variables “privadas”.

Por lo tanto este sistema, vendría a equivaler a la visibilidad private de C# para entendernos: Por defecto es imposible acceder a la propiedad, pero tenemos un mecanismo (en C# es Reflection) que nos permite saltarnos esa protección y acceder al contenido de la variable “privada”.

Weak maps

El uso de weak maps nos permite tener variables completamente privadas. Para ello usamos el nuevo tipo WeakMap de ES2015, que básicamente es un diccionario clave, valor, pero es especial porque no impide al GC de JavaScript recolectar todos aquellos valores almacenados en el weak map, siempre y cuando la referencia a la clave que tenga el weak map sea la única restante. Es decir, a todos los efectos el weak map no cuenta para el GC: aunque un objeto esté almacenado en él, si la clave usada no está referenciado por ningún otro objeto, el GC lo podrá eliminar.

El código usando WeakMap es el siguiente:

  1. const privates = new WeakMap();
  2. export default class Foo {
  3.     constructor(name) {
  4.         privates.set(this, {name: name});
  5.     }
  6.     get name() {
  7.         return privates.get(this).name;
  8.     }
  9. }

La idea es muy sencilla: por cada objeto que se cree de Foo, se almacena un valor en el WeakMap privates. La clave de dicho valor es this, que es el propio objeto Foo que se crea. Este objeto (privates.get(this)) almacena todo el estado “privado” del objeto Foo en cuestión. Gracias al hecho de que privates es un WeakMap no hay memory leak: Cuando el objeto Foo deje de usarse podrá ser eliminado por el garbage collector, aunque esté referenciado por el WeakMap como una de las claves usadas (precisamente por ser un privates Weak Map).

La clave (nunca mejor dicho :P) del asunto está en usar this como clave del Weak Map privates.

Al igual que en el caso del símbolo, el Weak Map es una variable privada del módulo, por lo que no se puede acceder a fuera desde él. Y a diferencia del caso del símbolo, ahora si que no hay nada que nadie pueda hacer para acceder a la variable privada que almacena el valor del nombre.

Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *