Privacidad de atributos en Javascript

La seguridad es un tema que no debe ser tomado a la ligera. Quizás los programadores novatos desconozcan algunas de estas técnica de seguridad en JavaScript, e incluso uno que otro programador experimentado puede que también ignore estas prácticas. Sin embargo, mas allá de la importancia de conocer estos métodos es praticarlos día a día en nuestra programación.

Las propiedades del proyecto se dejaban tradicionalmente desprotegidas en JavaScript u ocultas, capturadas en un cierre. Los símbolos y los WeakMaps ofrecen otra alternativa.

Tanto Chrome (versión 36) como Firefox (versión 31) soportan WeakMaps. Chrome 36 soporta Símbolos pero es necesario habilitar JavaScript Experimental en chrome://flags/#enable-javascript-harmony. Firefox soporta símbolos de la versión 33.

Escenario sin protección

Las instancias de person creadas usando la función de abajo tendrán propiedades almacenadas directamente en ellas.

var Person = (function() {
function Person(name) {
this.name = name;
}

Person.prototype.getName = function() {
return this.name;
};

return Person;
}());

var p = new Person('John');
print('Person 1 name: ' + p.getName());
delete p.name;
print('Person 1 name: ' + p.getName() + ' — modified outside.');

Este enfoque tiene la ventaja de que todas las instancias de la Person son similares y el acceso a las propiedades de esas instancias puede ser optimizado. Pero por otro lado no hay propiedades privadas aquí – todas las propiedades del objeto pueden ser modificadas por código externo (en este caso – borrado).

Varias bibliotecas prefieren prefijar las propiedades que están destinadas a ser privadas con el carácter de subrayado (por ejemplo, _nombre).

Otros – como TypeScript – dependen del compilador para marcar todos los usos ilegales de una propiedad privada.

Propiedades de ocultación con cierres

Para aislar una propiedad de una modificación externa se puede utilizar un cierre interno que se cierra sobre la variable name. Las convenciones de código de Douglas Crockford para JavaScript recomiendan este patrón cuando la privacidad es importante para disuadir a las propiedades de nombres con el prefijo de subrayado para indicar privacidad.


var Person = (function() {
function Person(name) {
this.getName = function() {
return name;
};
}

return Person;
}());

var p = new Person('John');
print('Person 2 name: ' + p.getName());
delete p.name;
print('Person 2 name: ' + p.getName() + ' stays private.');

El enfoque de cierre tiene la ventaja de la verdadera privacidad, pero el costo es que para cada caso de Person se tiene que crear un nuevo cierre (la función dentro del constructor de la Person).

Uso de símbolos

Con ES6 hay una forma más de almacenar propiedades: Símbolos.

Los símbolos son similares a los nombres privados pero -a diferencia de los nombres privados- no proporcionan verdadera privacidad.

Para ejecutar el ejemplo que ves tu navegador debe soportar:

  • ES6 Symbols ✅

var Person = (function() {
var nameSymbol = Symbol('name');

function Person(name) {
this[nameSymbol] = name;
}

Person.prototype.getName = function() {
return this[nameSymbol];
};

return Person;
}());

var p = new Person('John');
print('Person 3 name: ' + p.getName());
delete p.name;
print('Person 3 name: ' + p.getName() + ' — stays private.');
print('Person 3 properties: ' + Object.getOwnPropertyNames(p));

Los símbolos no aumentan el número de cierres por cada instancia creada. Sólo hay un cierre para proteger el símbolo.

Los símbolos se utilizan para indexar objetos JavaScript. La principal diferencia con otros tipos es que no son convertidos a cadenas y expuestos por Object.getOwnPropertyNames. Sólo utilizando el símbolo de referencia se pueden fijar y recuperar valores del objeto. Se puede acceder a una lista de símbolos asignados para un objeto determinado con la función Object.getOwnPropertySymbols.

Cada símbolo es único, incluso si se ha creado con la misma etiqueta.

ES6 Symbols ✅


var sym1 = Symbol('a');
var sym2 = Symbol('b');
var sym3 = Symbol('a');

print('sym1 === sym1: ' + (sym1 === sym1));
print('sym1 === sym2: ' + (sym1 === sym2));
print('sym1 === sym3: ' + (sym1 === sym3));

Los símbolos tienen las siguientes desventajas:

  • Mayor complejidad en la gestión de símbolos – en lugar de un simple p.name, primero hay que obtener la referencia del símbolo y luego usar p[nameSymbol].
  • Actualmente sólo unos pocos navegadores soportan símbolos.
  • No garantizan una verdadera privacidad, pero pueden utilizarse para separar las propiedades públicas de las internas de los objetos. Es similar a cómo la mayoría de los lenguajes orientados a objetos permiten el acceso a propiedades privadas a través de la API de reflexión.

Los símbolos privados todavía se consideran para ECMAScript, pero la implementación adecuada que nunca filtra los símbolos es difícil. Los símbolos privados ya son utilizados por la especificación ES6 y se implementan internamente en V8.

Uso de WeakMaps

Otro enfoque para almacenar propiedades privadas es WeakMaps.

Una instancia de WeakMap está oculta dentro de un cierre y es indexada por instancias de person. Los valores del mapa son objetos que contienen datos privados.


var Person = (function() {
var private = new WeakMap();

function Person(name) {
var privateProperties = {
name: name
};
private.set(this, privateProperties);
}

Person.prototype.getName = function() {
return private.get(this).name;
};

return Person;
}());

var p = new Person('John');
print('Person 4 name: ' + p.getName());
delete p.name;
print('Person 4 name: ' + p.getName() + ' — stays private.');
print('Person 4 properties: ' + Object.getOwnPropertyNames(p));

Es posible utilizar Map en lugar de un WeakMap o incluso un par de arrays para imitar esta solución. Pero el uso de WeakMap tiene una ventaja significativa – permite que las instancias de Person sean recolectadas como basura.

Un Mapa o una matriz sostiene objetos que contienen con fuerza. Person es un cierre que captura la variable privada – que es también una referencia fuerte. El recolector de basura puede recoger un objeto si sólo hay referencias débiles a él (o si no hay ninguna referencia en absoluto). Debido a las dos fuertes referencias, siempre y cuando la función Person sea alcanzable desde las raíces de la CG, entonces cada instancia de Person jamás creada es alcanzable y por lo tanto no puede ser recolectada como basura.

El WeakMap mantiene las claves débilmente y eso hace que tanto la instancia de Person como sus datos privados sean elegibles para la recolección de basura cuando un objeto Person ya no es referenciado por el resto de la aplicación.

Acceso a propiedades de otras instancias

Todas las soluciones presentadas (con excepción de los cierres) tienen una característica interesante. Las instancias pueden acceder a propiedades privadas de otras instancias.

El siguiente ejemplo clasifica las instancias de Person por sus nombres. La función compareTo utiliza los datos privados de esta y otras instancias.


var Person = (function() {
var private = new WeakMap();

function Person(name) {
var privateProperties = {
name: name
};
private.set(this, privateProperties);
}

Person.prototype.compareTo = function(other) {
var thisName = private.get(this).name;
var otherName = private.get(other).name;
return thisName.localeCompare(otherName);
};

Person.prototype.toString = function() {
return private.get(this).name;
};

return Person;
}());

var people = [
new Person('John'),
new Person('Jane'),
new Person('Jim')
];

people.sort(function(first, second) {
return first.compareTo(second);
});

print('Sorted people: ' + people.join(', '));

El mismo ejemplo escrito en Java:


import java.util.Arrays;

class Person implements Comparable<Person> {
private String name;

public Person(String name) {
this.name = name;
}

public int compareTo(Person other) {
return this.name.compareTo(other.name);
}

public String toString() {
return this.name;
}
}

public class Main {
public static final void main(String... args) {
Person[] people = new Person[] {
new Person("John"),
new Person("Jane"),
new Person("Jim")
};

Arrays.sort(people);

System.out.print("Sorted people: " + Arrays.toString(people));
}
}

El método Person::compareTo utiliza el nombre de campo privado de esta instancia y de otro objeto.

Felicidades por haber finalizado este nuevo tutorial, recuerda que en tu web escuelaJavaScript.com tendrás acceso a los mejores cursos de lenguaje de programación JavaScript.