Ya hemos dicho que en Javascript todo se comporta como un objeto, y que a cualquier objeto se le puede asignar de forma dinámica una propiedad. Hemos visto como podemos asignar prototipos a funciones para lograr herencia y como crear objetos componiéndolos con una función factory.

Sin embargo hay otra cualidad que nos aporta la orientación a objetos y de la que no hemos hablado aunque si hemos visto: la encapsulación. ¿Y ésto qué es? Intentaré explicarlo a través de sus dos fines.

Abstracción

El fin de un objeto, independientemente del lenguaje, es el de almacenar un estado y contener la lógica que sea capaz de modificarlo. Ésto hace que el objeto pueda ser autónomo y pueda interactuar con otros sin que el resto sepa como funciona internamente. Es lo que se conoce como abstracción. Un ejemplo.

Tenemos un objeto mike y un objeto car que dispone de dos métodos, gas y brake, y el atributo speed. Ambos objetos interactuan a través de las acciones que car provee. mike hace que car aumente su velocidad a través del método gas y que la disminuya a través de brake. Sin embargo mike desconoce como hace car para lograr ésto, no sabe como funciona un motor ni los frenos.

const car = {  
  speed: 0,
  gas() { /* magic stuff */ },
  brake() { /* magic stuff */ }
};

const mike = { name: 'Mike', car };

mike.car.gas();  
mike.car.speed // 1  

Ya, pero mike puede modificar la propiedad speed de car y ahorrarse todo eso. Es cierto, podríamos ahorrarnos los métodos y hacerlo directamente pero ¿Y si para modificar speed tuviéramos que modificar otras cosas antes? Igual no es una buena idea que mike pueda modificar speed...

Privacidad

La encapsulación no solo nos abstrae de la lógica de un objeto sino que permite que permanezca oculta y que los demás solo vean lo que nosotros queremos. Es lo que se conoce como privacidad.

Si volvemos al ejemplo de antes vemos que mike es capaz de modificar la propiedad speed de car sin usar sus métodos para ello. ¡Claro, porque en Javascript todas las propiedades de un objeto son accesibles! Pero esto no siempre interesa. mike podría acabar rompiendo car si no sabe cómo hacerlo de forma correcta, así que vamos a impedírselo. Añadimos un nuevo método getSpeed para que mike sepa la velocidad pero no pueda modificarla él mismo. Para ello haremos uso de nuevo de nuestras amigas las funciones factory.

Nota: A partir de ahora, ya que expliqué por qué evitar el uso de funciones constructoras en favor de las factory, haré uso siempre que pueda de las funciones "fat arrow" de ES6 ya que simplifican la sintaxis.

const car = () => {  
  let speed = 0;
  return {
    gas() { speed += 1 },
    brake() { speed -= 1 },
    getSpeed() { return speed }
  };
};

const mike = { name: 'Mike', car: car() };  

What? Ya te digo que eso no va a funcionar, speed solo se define cuando se ejecuta la función y te va a provocar una excepción... ¿Seguro?

mike.car.getSpeed(); // 0  
mike.car.gas();  
mike.car.getSpeed(); // 1  

Vale... funciona pero te estás quedando conmigo. ¡Seguro que car tiene la propiedad speed!

mike.car.speed;       // undefined  
mike.car.speed = 10;  
mike.car.getSpeed();  // 1  

WTF? ¡Pero si la he modificado! ¿Cómo es que sigue siendo "1"? Realmente lo que hemos hecho ha sido asignar la propiedad speed a car, no modificar el speed del contexto de la función. Vale ya entiendo, pero si creas otro car se va a volver a ejecutar y sobreescribes el de antes...

const john = { name: 'John', car: car() };  
john.car.getSpeed(); // 0  
mike.car.getSpeed(); // 1

mike.car.gas();  
john.car.getSpeed(); // 0  
mike.car.getSpeed(); // 2  

Ahora si que no entiendo nada... Claro, porque me he saltado explicar una de las cosas que implementa Javascript: los closures.

Closures

Los closures, que traduciríamos como cierres o clausuras, es lo que nos garantiza que el contexto de una función se mantenga activo aunque la función ya se haya ejecutado. ¿Cómo? Haciendo que dicha función devuelva otra función que acceda al contexto de la primera. Menudo trabalenguas... Suena todo demasiado complejo. Es más simple de lo que suena, veámos otro ejemplo.

Queremos tener un contador que incremente únicamente de 1 en 1. Para ello llamaremos a una función increment que se encargará de hacerlo. El contador se usará para una estadística y los usuarios no deben conocer cuantas veces lo han accionado. Por tanto vamos a provocar un closure que garantice la privacidad.

Nota: Podemos usar las "fat arrow" de forma simple en una línea para devolver funciones anónimas que solo hagan una cosa.

const counter = () => {  
  let times = 0;
  return () => { times += 1 };
};

const increment = counter();  
increment();  // times = 1  
increment();  // times = 2  

La función que devolvemos está definida en el contexto de la función counter, por tanto es capaz de acceder al resto de cosas que también estén definidas en él, como el objeto times. Si hacemos que esa función acceda a time y luego la devolvemos, provocamos un closure.

La función devuelta la asignamos a increment para mantener el closure creado. Cada vez que llamemos a increment se aumentará en 1 el objeto times. Como times no es accesible y no podemos comprobarlo vamos a modificarla para asegurarnos que funciona.

const counter = () => {  
  let times = 0;
  return () => { 
    times += 1;
    console.log(times);
  };
};

const increment = counter();  
increment();  // 1  
increment();  // 2  

Si quisiéramos crear un nuevo contador podemos ejecutar de nuevo counter y asignárselo a otra variable, crearíamos un nuevo closure independiente al del primero.

const anotherIncrement = counter();  
anotherIncrement(); // 1  
increment();        // 3  

En este caso sólo devolvemos una función, pero al igual que en el ejemplo anterior con car podríamos usar una función factory para devolver objetos que accedieran a su contexto mediante métodos y crear tantas variables como sea necesario.

const car = () => {  
  let speed = 0;
  let started = false;
  return {
    gas() { speed += 1 },
    brake() { speed -= 1 },
    getSpeed() { return speed },
    isStarted() { return started }
  };
};

Así garantizamos que el estado se mantenga oculto y que solo se pueda modificar a través de los métodos que hacemos públicos. Podríamos crear tantos objetos como queramos con car y todos mantendrían su propio estado oculto al resto.

Conclusión

La encapsulación es una de las mayores ventajas de la orientación a objetos. Nos permite mantener la integridad de un objeto mediante la privacidad y controlar su interacción con otros objetos mediante la abstracción.

En Javascript podemos implementarla a través de funciones que creen closures. Sin embargo no es posible lograr privacidad si queremos hacer uso de herencia, no al menos de una manera sencilla. Siempre tendremos que recurrir a la composición para lograrlo y con algunas limitaciones.

Con éste capítulo doy por finalizada la teoría sobre creación y composición de objetos y pasamos a otro de los aspectos potentes e importantes de Javascript: las funciones.

"Sólo hay una persona que puede decidir lo que voy a hacer, y soy yo mismo". - Citizen Kane (1941)