POO en Javascript

04 de enero 2019ComentariosjavascriptDavid Poza SuárezComentarios

En artículo de hoy voy a tratar la implementación clásica que realiza Javascript de la de orientación a objetos, y que es importante conocer antes de meterse con librerías o frameworks. No obstante en artículos posteriores volveré a trata estos mismos conceptos pero usando las novedades que plantea ES5, ES6 y consecutivos.

Empezamos definiendo conceptos clave.

La programación orientada a objetos (POO) es un modelo o filosofía de lenguaje basado en el concepto de objeto, el cual puede contener información (propiedades o atributos) y un comportamiento (métodos o funciones), de modo muy similar a los objetos del mundo real. Un ejemplo super típico sería uno en que el objeto es un coche, con las propiedades (cilindrada, nºplazas, color, etc.) y con los métodos (acelerar, frenar, cambiar de marcha, etc).

En POO hay varios conceptos básicos, que de forma muy esquemática, son:

  • Clase: Es el molde, o el patrón que define qué propiedades y métodos va a tener un objeto que sea una instancia de la esta clase.
  • Herencia: Nos permite crear una subclase(clase hija) que posea automáticamente las propiedades y métodos de la superclase o clase padre. De forma que reutilizamos código.
  • Polimorfismo: Podemos ejecutar un mismo método en objetos distintos sin importar qué tipo tienen porque en cada caso se ejecuta la implementación correcta.
  • Encapsulamiento: Es la capacidad de definir propiedades y métodos privados dentro de un objeto. Que serán únicamente accesibles desde dentro de la función donde se han definido.

Un lenguaje de POO puede basarse en clases o en prototipos. En el primer caso tenemos ejemplos como Java, C++ o Php, mientras que en el segundo tenemos otros como Javascript, que es el que nos interesa en esta ocasión.

Prototype y herencia

En Javascript no tenemos clases (aunque en ES6 ya podemos usar la sintaxis de class, internamente todo sigue funcionando del mismo modo), únicamente existen los objetos, todo es un objeto. En su lugar, un objeto logra la herencia por medio del atributo prototype, una referencia a otro objeto, su prototipo.

Un ejemplo donde vemos la herencia y el polimorfismo:

//Hablaremos de clases para entendernos, aunque no lo son propiamente, siempre son objetos.

// Super Clase
function Persona(nombre, edad, peso) {
    this.nombre = nombre;
    this.edad = edad;
    this.peso = peso;
    this.getInfo = function() {
      return "Me llamo " + this.nombre +
             ", tengo " + this.edad + " años " +
             "y peso " + this.peso + " kilos.";
    }
}

// Sub Clase
function Empleado(nombre, edad, peso, sueldo){
    Persona.call(this, nombre, edad, peso); //llamamos al constructor de Persona
    this.sueldo = sueldo;
    let clave\_acceso = ""; //propiedad privada

    this.setClaveAcceso = function(key){
        clave\_acceso = key;
    }
    this.sueldo = sueldo;
    this.getInfo = function() {
      return "Me llamo " + this.nombre +
             ", tengo " + this.edad + " años " +
             ", peso "+ this.peso + " kilos y gano " + this.sueldo + " euros.";
    }
}

// Herencia
Empleado.prototype = Object.create(Persona.prototype);
Empleado.prototype.constructor = Empleado;

// Polimorfismo
// objeto puede ser de tipo Persona o de tipo Empleado, y getInfo llamará a la implementación correspondiente.
function showInfo(objeto) {
    console.log(objeto.getInfo());
}

// Instancias de un clase
let persona = new Persona("Javi", 31, 90);
let empleado = new Empleado("David", 32, 91, 23000);
showInfo(persona);
showInfo(empleado);

Herencias múltiples

En javascript solo existe la herencia simple porque una clase solo puede tener un padre.

Concatenar objetos

Pongamos que en el ejemplo anterior ahora queremos que Empleado herede de otra clase Informático además de Persona.

Lo primero sería llamar al constructor de Informático dentro del constructor de Empleado usando call, que permite llamar a una función pero especificando el contexto que queremos usar. De este modo todas las propiedades que tenga Informatico se crearan dentro de Empleado.

Informatico.call(this, ...);

Ya tendríamos las propiedades heredadas, ahora queda hacer lo mismo con los métodos. Para lo cuál vamos a usar la concatenación de objetos, de modo que añadimos el prototype de Informatico al prototipo existente de Empleado.

Object.assign(Informatico.prototype, Empleado.prototype);

Con estos dos pasos ya tendríamos un Empleado que se comporta además de como Persona como Informático, disponiendo de todas sus propiedades y métodos.

Mixins

Los mixins, que son clases que permiten añadir métodos a otras clases sin necesidad de ser su prototipo.

Continuando con el ejemplo de Persona y Empleado

// creamos un Mixin
let asInformatico = function(obj){
    obj.programar = () => {
        console.log("Estoy escribiendo código.");
    }
};

asInformatico(empleado);

//ahora el empleado puede comportarse como un programador además de ser una persona.
empleado.programar();

Identificador "this"

De nuevo vemos el ejemplo de la "clase" persona. Donde para referirnos a una propiedad o método de Pesona desde dentro de la misma, tenemos que usar el identificador this, que indica que va a buscar la propiedad en el contexto desde donde se ejecuta el método.

Si llamamos a persona.getInfo(), el contexto es persona, por lo que this apunta a persona y se encuentran this.nombre, this.edad y this.peso.

Hasta aquí todo es como cabría esperar. Pero ahora vamos a almacenar la referencia a la función getInfo en una variable y después a ejecutarla. Y, como podemos observar, obtenemos un error porque ahora no estoy indicando ningún contexto y por tanto el método se ejecuta en el contexto global y this apunta al objeto Window.

function Persona(nombre, edad, peso) {
    this.nombre = nombre;
    this.edad = edad;
    this.peso = peso;
    this.getInfo = function() {
      return "Me llamo " + this.nombre +
             ", tengo " + this.edad + " años " +
             "y peso " + this.peso + " kilos.";
    }
}
//contexto persona
let persona = new Persona("David", 31, 90);
console.log(persona.getInfo()); //resultado esperada

//contexto window
let getInfo = persona.getInfo;
console.log(getInfo()); //error nombre, edad y peso no existen

Este mismo problema sucede cuando estamos pasando funciones como parámetro (por ejemplo al añadir un listener a un evento). Esto es porque **estamos pasando la referencia al método** y cuando se ejecuta se ha perdido el contexto.

setInterval(persona.getInfo); //pasamos la referencia al método y da error

**La solución es usar bind para indicar qué contexto debe usar**. Si indicamos que use el contexto persona todo funcionará correctamente.

function Persona(nombre, edad, peso) {
    this.nombre = nombre;
    this.edad = edad;
    this.peso = peso;
    this.getInfo = function() {
      console.log("Me llamo " + this.nombre +
             ", tengo " + this.edad + " años " +
             "y peso " + this.peso + " kilos.")
    }
}
//contexto persona
persona = new Persona("David", 31, 90);

setInterval(persona.getInfo.bind(persona));

/*Otra solución, y la más típica, es usar una función anónima en línea y dentro de ella si que ejecutamos el método dejando claro que el contexto es persona*/

setInterval(function(){persona.getInfo()})

Si necesitamos pasar parámetros a una función de callback podemos usar bind e incluirlos después del contexto, que debe ser el primer parámetro.

setInterval(persona.getInfo.bind(persona, "Hola"));

Como segunda opción podemos pasar los parámetros creando otra función envoltorio que retorne una referencia a una función anónima con la llamada a la función deseada incluyendo los parámetros.

function callback(saludo){
  return function(){
    persona.getInfo(saludo);
  }
}

setInterval(callback("Hola")); //resultado esperado

Por último otra opción para indicar explícitamente el contexto al ejecutar una función es usar call, que lleva como primer parámetro el contexto y a continuación los argumentos que llevase la función llamada.

Un ejemplo de uso de call es cuando creamos una subclase y queremos llamar al constructor de la clase padre en el constructor de la clase hija.

Namespaces y módulos

Javascript ya no se usa solamente para hacer pequeños scripts, ahora se construyen grandísimas aplicaciones que requieren organizarse bien.

Surge la necesidad de separar el código de las diferentes librerías y el nuestro mismo, de forma que aunque se repitan nombres de variables y funciones, todo siga funcionando correctamente sin que aparezcan conflictos. Para resolver el problema aparecen los namespaces y los módulos.

Como en Javascript todo es un objeto, también usamos un objeto como espacio de nombres o namespace. Lo hacemos del siguiente modo:

const MINAMESPACE = MINAMESPACE || {};
MINAMESPACE.subnamespace= {} ;

Otra opción más cómoda para poder crear namespaces con varios niveles sería esta:

function createNameSpace( nameSpaceString ) {
    let names = nameSpaceString.split(".");

    //global object is the parent for first level nameSpace
    let parent = window;

    //if any nameSpace level doesn't exist, create it
    for ( let i=0, imax=names.length ; i < imax; i++ ) {
        if ( !parent\[names\[i\]\] ) parent\[names\[i\]\]={};
        parent = parent\[names\[i\]\];
    }
}

createNameSpace( "MINAMESPACE.UTILS.ajaxHandler" );

Una vuelta de tuerca al uso de namespaces es el patrón módulo (introducido por Douglas Crockford), que nos permitirá tener variables y funciones privados dentro del espacio de nombres.

Para ello usamos una IIFE (función inmediatamente invocada o función auto-ejecutable):

const MINAMESPACE = MINAMESPACE || {};
MINAMESPACE = (function () {
   let \_varprivada1 = 1;
   let \_varprivada2 = "dos";

   function metodoPrivado() {
     // método privado
   };
   return {
       varpublica: " una variable pública ",
       metodoPublico: function () {
        //aqui podemos usar las funciones y variables privadas
       }
   };
})();

Como podemos ver las partes públicas se definen dentro del return, por lo que si necesitamos cambiar la visibilidad de alguno de ellas tendremos que andar moviendo bloques de código, lo que no resulta del todo práctico.

En este punto surge el patrón módulo revelado (revealing module pattern), que permite mantener todo el código dentro del cuerpo del objeto que define el módulo, teniendo únicamente que indicar la referencia de las partes públicas en el return. Veamos:

const MINAMESPACE = MINAMESPACE || {};
MINAMESPACE = (function () {
   let \_varprivada1 = 1;
   let \_varprivada2 = "dos";
   varpublica: " una variable pública "

   function metodoPrivado() {
     // método privado
   };

   metodoPublico: function () {
     //aqui podemos usar las funciones y variables privadas
   }

   return {
       varpublica: varpublica,
       metodoPublico: metodoPublico
   };
})();