Dónde están las clases en Javascript (II)


Decíamos en la anterior entrega que si entendemos que el prototipo es un puntero o referencia, es muy fácil entender que pasa si asignamos a Persona.prototype un nuevo objeto.

Persona.prototype. =  {
    casa : 'chalet en la playa',
    coche : 'tartana'
}
// ¿Qué ghabrá pasado con las antiguas propiedades del prototipo?
console.log(pedro.empleo);       // >>> programador
console.log(juan.empleo);        // >>> ninguno
// Mmmmm! parece que siguen ahí
// Creemos un nuevo objeto a ver
var alex = new Persona('alex');
console.log(alex.empleo);        // >>> undefined
console.log(alex.cobrar());      // >>> undefined
// ¡Vaya! pues no están en alex
// ¿Y qué pasa con las nuevas en nuestros viejos amigos pedro y juan?
console.log(pedro.casa);         // >>> undefined
console.log(juan.casa);          // >>> undefined
console.log(pedro.coche);        // >>> undefined
console.log(juan.coche);         // >>> undefined
// ¡Sorpresa! juan y pedro no pueden tener ni coche ni casa
// ¡Normal! Uno está en el paro y el otro es programador ¿Qué esperaban?
// ¿Habremos hecho algo mal?
console.log(alex.casa);          // >>> chalet en la playa
console.log(alex.coche);         // >>> tartana

Creo que en vez de soltar una parrafada va a ser mejor dejar un diagrama que explique la nueva situación.

Figura 4

Utilizar la herencia de forma eficiente

Pero ¿y dónde están mis viejas y amadas clases? Esas con métodos que puedo extender y sobrescribir
Errrr,… ¿no dije que en javascrtipt no hay clases? No sólo no las hay, si no que las propiedades de los objetos hasta donde hemos visto son todas públicas (lo cual tiene fácil remedio), todo es dinámico, siempre se pueden sobreescribir las propiedades (No hay forma de protegerlas), y un largo etdétera que hace absurdo seguir pensando como cuando programamos en un lenguaje con clases.
Podemos, eso sí, pensar en el tipo de relación que queremos entre los objetos, que puede ser conceptualmente la misma que querríamos entre nuestras clases o nuestras instancias en un lenguaje basado en clases, y tratar de crear esa relación en javascript. De hecho, javascript demuestra ser muy flexible y potente para implementar cualquier tipo de relación entre objetos, haciendo que algunos patrones de diseño son especialmente fáciles de implementar.

Supongamos, entonces, que queremos crear una serie de modelos de objetos a partir de los cuales podamos generar diferentes tipos de instancias de objeto. Probablemente unos tipos tendrán propiedades en común con otros. ¿Será la herencia a través de los prototipos la mejor manera de expresar esa relación?. Depende. Aunque estemos aprendiendo a utilizar la herencia en javascript no hay que olvidar que uno de los pilares de la programación orientada a objetos es la recomendación de favorecer la composición antes que la herencia. Pero centrémonos en un ejemplo en el que tenga sentido hablar de herencia aunque no sea, necesariamente, la mejor aproximación posible.

Utilizar la herencia de forma eficiente

Imaginad que queremos construir una serie de objetos que expresen la relación entre los seres vivos del planeta. Como hay muchas clasificaciones posibles vamos a centrarnos básicamente en quién se come a quién, para que el proyecto sea asequible.

Figura 5

Si decidimos que cada uno de los objetos que creemos va a caer en un a de las cuatro categorías de la figura 5, tendremos que crear 4 tipos de objeto, y tener acceso a cada una de las propiedades que aparecen en la figura 6.

Parece un poco redundante, ¿no? Si creamos una instancia por cada especie desperdiciaremos un monton de memoria. Cierto es que todas ellas deberán albergar una copia de las propiedades que almacenan valores, especie, reino, etc. Pero es un desperdicio que todas las instancias almacenen una copia de todas las funciones.
Esto es lo que ocurriría si símplemente implementáramos todas las propiedades en los constructores de cada tipo de animal:

var Productor = new function (especie, reino, medio) {
	this.especie = especie;
	this.reino = reino;
	this.clase_trofica = 'productor';
	this.medio = medio;
	this.nacer = function () {
		console.log('Un nuevo espécimen de ' + this.especie + ' ha nacido.');
	};
	this.morir = function () {
		console.log('Un espécimen de ' + this.especie + ' acaba de morir.');
	};
	this.alimentarse = function () {
		console.log('Este espécimen de ' + this.especie + ' se está alimentando');
	};
	// notar que los productores, por ser vegetales no se desplazan
};
var Herbivoro = new function (especie, reino, medio) {
	this.especie = especie;
	this.reino = reino;
	this.clase_trofica = 'herbívoro';
	this.medio = medio;
	this.nacer = function () {
		console.log('Un nuevo espécimen de ' + this.especie + ' ha nacido.');
	};
	this.morir = function () {
		console.log('Un espécimen de ' + this.especie + ' acaba de morir.');
	};
	this.alimentarse = function () {
		console.log('Este espécimen de ' + this.especie + ' se está alimentando');
	};
	this.dormir = function () {
		console.log('Este espécimen de ' + this.especie + ' está durmiendo');
	};
	this.desplazarse = function () {
		console.log('Este espécimen de ' + this.especie + ' se está desplazando');
	};
};

// ...
// Los constructores de carnívoros y necrófagos serían idénticos al de herbívoros
// salvo por el valor al que inicializan la propiedad clase_trofica.

Si nos ponemos a crear las especies de la figura 6 a diestro y siniestro usando estos constructores precedidos de new, estaremos ‘clonando’ 20 veces las funciones nacer() morir() y alimentarse(), 15 veces las funciones dormir() y desplazarse(), además de otras propiedades que pueden ser comunes a varias especies. Si añadimos nuevas especies no haremos sino incrementar el desperdicio de memoria.

El primer defecto evidente de este código es que estamos definiendo las funciones dentro del costructor. Deberíamos hacerlo en el prototipo del que van a heredar los objetos que creemos. Por ejemplo:

var Productor = new function (especie, reino, medio) {
	this.especie = especie;
	this.reino = reino;
	this.clase_trofica = 'productor';
	this.medio = medio;
};
Productor.prototype.nacer = function () {
	console.log('Un nuevo espécimen de ' + this.especie + ' ha nacido.');
};
Productor.prototype.morir = function () {
	console.log('Un espécimen de ' + this.especie + ' acaba de morir.');
};
Productor.prototype.alimentarse = function () {
	console.log('Este espécimen de ' + this.especie + ' se está alimentando');
};

Si hacemos esto también con el resto de los constructores, tendremos sólo 4 copias de nacer() morir() y alimentarse() en memoria y 3 de dormir() y desplazarse(), y además no importará el número de especies que creemos, este número de copias permanece constante, porque estas funciones están el los prototipos de las especies, y sólo hemos creado 4.

Pero lo más eficiente sería que las propiedades que contengan valores o funciones comunes a uno o varios grupos de objetos se almacenen en un prototipo común y que nos sirvamos del enlace secreto de cada objeto al mismo para recuperarlas.

Vayamos, pues, un poco más allá en nuestro análisis: Las funciones nacer() y morir() son comunes a todos los seres vivos, si las consideramos símplemente como el principio y el fin de existencia.
alimentarse() podría implicar tareas diferentes para un productor (normalmente del reino vegetal) que para un herbívoro o un carnívoro.
Hay más diferencias entre el reino animal y el vegetal que conviene tener en cuenta; por ejemplo: los animales duermen y se pueden desplazar.
Incluso dentro de cada clase trófica hay tareas específicas asociadas a la necesidad de alimentars que han de realizarse antes o después de alimentarse.

Evidentemente, hay muchos más factores que diferencian a unas especies de otras y podríamos seguir complicando el esquema hasta el infinito. Por otro lado, si pensamos en como será la implementación detallada del programa, probablemente surjan muchos más factores a tener en cuenta. Esto es algo que debemos hacer cuando tenemos entre las manos una aplicación real, pero para el propósito de este artículo nos basta con el análisis anterior.

La figura 7 muestra un esquema mucho más eficiente. Cada una de las tablas de propiedades representa un prototipo, con las propiedades que heredará, a través del enlace secreto (la línea roja), el objeto (sea un prototipo o una instancia de objeto).

Las únicas propiedades que deberían poseer como propias cada uno de los seres vivos son especie y medio, todas las demás las heredarán de alguno de los objetos de su cadena de prototipos. Tenemos dos opciones: crear las propiedades amano en cada objeto, o definirlas e inicializarlas en el constructor de la clase trófica correspondiente con dos parámetros. Optaremos por la segunda opción por que parece bastante más cómoda ¿no?

Vamos probar a implementar el esquema anterior a los vegetales a ver que tal nos va:

var Ser_vivo = function () {};
// Añadimos al prototipo las propiedades que queremos que sean compartidas por todos los seres vivos
Ser_vivo.prototype = {
    nacer : function () {
        console.log('El espécimen de ' + this.especie + ' ha nacido.');
    },
    morir : function () {
        console.log('El espécimen de ' + this.especie + ' acaba de morir.');
    },
    alimentarse : function () {
        console.log('El espécimen de ' + this.especie + ' se está alimentando');
    }
};

// Definimos el constructor de vegetales.
var Vegetal = function () {};
// Asignamos al prototipo de todo vegetal una instancia de ser vivo,
// para implementar la herecia de las propiedades que queremos compartir.
Vegetal.prototype = new Ser_vivo();
// Añadimos al prototipo las propiedades específicas de los vegetales
Vegetal.prototype.reino = 'vegetal';
Vegetal.prototype.buscar_luz = function() {
        console.log('El espécimen de ' + this.especie + ' está buscando una fuente de luz.');
};
Vegetal.prototype.hacer_fotosintesis = function() {
        console.log('El espécimen de ' + this.especie + ' está haciendo la fotosíntesis.');
};

// Definimos el constructor de productores.
var Productor = function (especie, medio) {
    this.especie = especie;
    this.medio = medio;
};
Productor.prototype = new Vegetal();
Productor.prototype.clase_trofica = 'productor';

var trigo = new Productor('trigo', 'terrestre');

Si enumeramos los las propiedades del objeto trigo con un bucle for in obtendremos(ver apéndice al final):

// especie: trigo
// medio: terrestre
// clase_trofica: productor
// reino: vegetal
// buscar_luz: function () { ... }
// hacer_fotosintesis: function () { ... }
// nacer: function () { ... }
// morir: function () { ... }
// alimentarse: function () { ... }

Si filtramos con hasOwnProperty() la lista anterior para el trigo:

// especie: trigo
// medio: terrestre

Si filtramos con hasOwnProperty() la lista de propiedades de Productor.prototype:

// clase_trofica: productor

Si filtramos con hasOwnProperty() la lista de propiedades de Vegetal.prototype:

// reino: vegetal
// buscar_luz: function () { ... }
// hacer_fotosintesis: function () { ... }

Y si hacemos lo propio com Ser_vivo.prototype

// nacer: function () { ... }
// morir: function () { ... }
// alimentarse: function () { ... }

Todo parece correcto ¿no?, el resultado es el quequeríamos ¿verdad?. Bueno, veremos que tiene algunos defectos en la siguiente entrega.

Apéndice

He aquí un par de funciones de ayuda para listar todas las propiedades de un objeto:

var enumerate = function(objeto, nombre, showValor, onDoc) {
    var title = 'MIEMBROS DEL OBJETO ' + (nombre || '??');
    if (onDoc){
        document.writeln(title)
    } else {
        console.log(title);
    }
    for (var propName in objeto) {
        var propOutput = propName;
        if  (showValor !== undefined) {
            propOutput += ': ' + objeto[propName];
        }
        if (onDoc) {
            document.writeln( propOutput );
        } else {
            console.log(propOutput);
        }
    }
};

y sólo las que son propias (no heredadas)

var enumerateOwn = function(objeto, nombre, showValor, onDoc) {
    var title = 'MIEMBROS PROPIOS DEL OBJETO ' + (nombre || '??');
    if (onDoc){
        document.writeln(title)
    } else {
        console.log(title);
    }
    for (var propName in objeto) {
        if (objeto.hasOwnProperty(propName))
        {
            var propOutput = propName;
            if  (showValor ) {
                propOutput += ': ' + objeto[propName];
            }
            if (onDoc) {
                document.writeln( propOutput );
            } else {
                console.log(propOutput);
            }
        }
    }
};

Funcionan con este index.html

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
<h1></h1>
    <pre><script type="text/javascript" src="js/PrototypalInheritance.js"></script></pre>
</body>
</html>

Referencias

ECMA-262-3 in detail. Chapter 7.2. OOP:ECMAScript Implementation por Dmitry Soshnikov
El original está en ruso y la versión en inglés es un poco descuidada en la redacción, pero está muy bien estructurada y aclara muchos conceptos. Es, sin embargo, una lectura para iniciados. Y no os engaño si confieso que, aunque creo haber captado lo fundamental, no lo he entendido todo.

Understanding JavaScript Prototypes. por Angus Croll
Escrito a modo de FAQ, también es muy claro y además conciso.
[Actualización] ¡Sorpresa!, Alguien lo ha traducido al español. No lo he revisado, pero aquí está el enlace.

The Javascript Programming Language por Douglas Crockford (VIDEO)
Advanced JavaScript por Douglas Crockford (VIDEO)
Casi enciclopédicos, pero muy interesantes.

3 comentarios en “Dónde están las clases en Javascript (II)

  1. Pingback: Pregunta: ¿Dónde están las clases en Javascript? (I) « Newbe forever

  2. Y si usamos el prototipo del prototipo ? objeto.__proto__.__proto__=Objeto. El último eslabón de la cadena de prototipos no es Object, es Object.prototype.
    Saludos.

    Responder
    • La propiedad __proto__ no forma parte del estándar de javascript y solo funciona en algunos navegadores, así que todos deberíamos dejarla fuera de la ecuación

      Responder

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s