Pregunta: ¿Dónde están las clases en Javascript? (I)


Respuesta: No hay.
Javascript es un lenguaje basado en prototipos, no en clases. Eso lo tengo claro. Lo que son los prototipos ya es otro cantar, y de eso es de lo que quiero hablar.

Antes de empezar debe quedar claro que estas líneas son el resultado de mi intento por entender el tema; y sin ser ningún experto y teniendo en cuenta que el cacao de tutoriales, guías y manuales que hay desperdigados por la red son en su mayoría una soberana $&@#~… espero que estas notas despejen algunas dudas. Al menos a mi me han ayudado a centrar mi compresión del lenguaje. Sobre todo para lo que era mi objetivo: ser capaz de hacer lo que ya era capaz de hacer en actionscript.

Para empezar, parece que la mejor manera de entender la herencia basada en prototipos es olvidarse de todo lo que sabemos de herencia basada en clases y empezar de cero.

Objetos

En javascript para definir la estructura de cualquier información y lo que se puede hacer con ella usamos objetos. Un objeto es una colección no ordenada de pares clave : valor. Si una variable no es una primitiva ( undefined, null, null, Boolean, Number o String) es un objeto (las funciones también). Los objetos se pueden definir de dos maneras:

// utilizando el "literal de objeto":
var persona = {
    nombre : 'persona',
    getNombre : function() {
        return this.nombre;
    }
};

 

// La forma anterior es equivalente a esta (que usa el constructor
// predefinido de objetos):
var persona = new Object();
persona.nombre = 'persona';
persona.getNombre = function() {
    return this.nombre;
};

Así, nuestro objeto persona tiene dos propiedades: una cadena llamada nombre y una función llamada getNombre. La especificación ECMAScript no diferencia entre propiedades y métodos.
Cada objeto puede heredar propiedades de otro objeto, al que se llama prototipo (no confundir con la propiedad prototype de las funciones).

Todos los objetos que creamos tienen un prototipo, una referencia oculta que el interprete de javascript sigue para buscar las propiedades que no encuentra en nuestro objeto. Así, el prototipo de persona es el objeto nativo de javascript: Object.

Nota: Todos los objetos nativos de javascript heredan de Object en último término, y este último es el único objeto que no tiene prototipo, ya que está en la cúspide de cualquier cadena de herencia.

Esto significa que persona hereda todas las propiedades Object como se ve en la figura 1. (Recordar que en javascript una propiedad puede ser de tipo Function, lo que en otros lenguajes se denominaría un método)

Figura 1
Gracias a los prototipos podemos acceder a propiedades como constructor, hasOwnProperty([String]), etc. desde cualquier objeto.

console.log(persona.hasOwnProperty('nombre')); // >>> true

Herencia a través del prototipo

Las propiedades del objeto persona que antes hemos definido no pueden ser heredadas. Bueno, en realidad sí, pero hay que dar un buen rodeo. Después veremos como.

Aunque no hay clases en javascript, sí que hay herencia. Y es precisamente a través de los prototipos como se implementa. El problema es que no se puede acceder al prototipo de un objeto (al menos no de forma sencilla) a menos que se cree mediante una función constructora.

Un constructor es una función que devuelve un objeto con una estructura y valores iniciales predefinidos dentro de dicha función. Se parece al constructor de una clase en algunos aspectos, pero difiere mucho en otros.

Para generar un objeto a partir de un constructor utilizamos la palabra clave new, segida del nombre de la función constructora:

// Los nombre sde constructor se empiezan por mayúscula por convención,
// para distiguirlos del resto de las funciones.
var Persona = function(nombre) {
    this.nombre = nombre;
    this.getNombre = function () {
        return this.nombre;
    };
};
// Creemos un par de objetos con nuestro reluciente constructor
var pedro = new Persona('Pedro');
var juan = new Persona('Juan');

Sobra decir, creo, que no tiene sentido crear un constructor para un solo objeto. Lo único que se conseguiría es crear confusión.

Si solo necesitas una instancia de un objeto, no pierdas el tiempo creando un constructor

Como todas las funciones, el constructor tiene un valor de retorno. Cuando se utiliza con new éste es, por defecto, lo que quiera que contenga el objeto al que apunta this. Podríamos especificar un valor de retorno personalizado con un return pero esto nos crearía serios quebraderos de cabeza y además no nos va a ser muy util en este contexto.

¡Importante! Si no se utiliza con new, la función constructor devuelve undefined.

Este es el primer problema de aproximarse a la creación de objetos de esta forma. Es muy fácil olvidar el new. Más adelante explicaré como encapsular la creación de objetos de forma que no cometamos este error.

Por otro lado, al utilizar la palabra new tenemos la falsa impresión de estar trabajando con clases. Y más vale que nos quitemos esa noción de la cabeza. Especialmente cuando queremos trabajar con herencia. Vamos a ver por qúe.

El constructor Persona nos permitirá crear objetos con los mismos miembros que teníamos definidos en el objeto inicial persona. Pero además de añadir propriedades directamente al objeto podremos añadir nuevas propiedades y métodos a su proptotipo, gracias a la propiedad prototype de su constructor. Esta es la primera diferencia con los lenguajes clásicos (basados en clases), javascript es dinámico hasta para esto.

Veamos como funciona un constructor:
Como ocurre para cualquier función, al definir el constructor Persona se inicializa su propiedad Persona.prototype apuntando a un nuevo un objeto vacío, salvo por la propiedad constructor, que apunta a su vez a la mismísima función constructora.
A partir de ese momento podemos añadir propiedades (primitivas, objetos o funciones) al objeto al que apunta Persona.prototype.

¡Cuidado! Persona.prototype no apunta al prototipo de la función Persona, sino al prototipo de los objetos que creemos con ella al utilizarla como constructor (poniéndole new delante)

Al ejecutar new Persona('nombre') el interprete hace dos cosas:

  • Se crea un nuevo objeto y se inicializa con las propiedades y valores que se han definido para this dentro del constructor.
  • Se establece en el nuevo objeto un enlace secreto al prototipo asociado al constructor (al que apunta Persona.prototype). Las referencias ocultas de los objetos pedro y juan ya no apuntan al objeto por defecto si no a Persona.prototype.

La figura 2 ilustra la situación con los objetos pedro y juan recién creados.

Figura 2: pedro y juan heredan del prototipo definido por Persona.prototype
Añadamos algunas propiedades al prototipo de los objetos que crea el constructor Persona para probar:

Persona.prototype.empleo = 'ninguno';
Persona.prototype.cobrar = function () {
    var salario;
    if (this.empleo === 'ninguno') {
        salario = '¿acaso tienes subsidio?';
    } else if (this.empleo === 'programador') {
        salario = '¡Mejor juega a la lotería!';
    }
    return salario;
}
// Ahora podemos acceder a estas propiedades desde los objetos pedro y juan
// a traves de la referencia secreta a su prototipo.
console.log(pedro.empleo);       // >>> ninguno
console.log(pedro.cobrar());     // >>> ¿acaso tienes subsidio?
// Mal rollo para pedro, vamos  a darle un trabajo provechoso
pedro.empleo = 'programador';
console.log(pedro.cobrar());     // >>> ¡Mejor juega a la lotería!

Al añadir las propiedades empleo y cobrar al prototipo de los objetos pedro y juan, podemos acceder a ellas desde estos objetos, porque el intérprete de javascript al no encontrarlos directamente entre sus propiedades, sigue la denominada cadena de prototipos a traves de las referencias secretas de cada objeto y, en este caso, las encuentra en el prototipo de pedro.

Cuando escribimos explicitamente pedro.empleo = 'programador' estamos creando explícitamente la propiedad empleo en pedro e inicializándola a la cadena 'programador'.
Curiosamente el interprete ejecutará la función cobrar() desde el prototipo de pedro, pero dentro del ámbito del objeto de pedro. Este pequeño detalle es fundamental. Si no fuera así no podríamos hablar de herencia.

Otro detalle importante que no se nos puede escapar es que las propiedades nuevas son accesibles desde pedro y juan a pesar de que se añaden al prototipo después de haberlos instanciado.
A primera vista parecerá raro, pero no lo es si tenemos en cuenta que, tanto los enlaces secretos de pedro y juan a su prototipo, como la propiedad Persona.prototype del constructor, son referencias (o punteros) al objeto al que estamos añadiendo las propiedades.

Importante: Las referencias ocultas de los objetos a su prototipo y la propiedad prototype del constructor son punteros a un objeto.

La figura 3 ilustra la nueva situación:

Figura 3
Pues bien, si intentamos leer, por ejemplo, la propiedad juan.salario, el intérprete de javascript buscará primero en el objeto juan, y si no la encuentra, buscará en su objeto prototipo (el mismo al que apunta Persona.prototype) y si no buscará en el prototipo de su prototipo, … y así hasta llegar al objeto nativo por defecto object. Si no la encuentra en object devolverá undefined.

Ahora que 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

Bueno, parece que nos quedamos sin espacio. Habrá que dejar la incógnita en el aire hasta la próxima entrega.
Entonces hablaremos de este aparente sinsentido, de los objetos creados con un constructor, y de como aprovechar los prototipos para construir algo parecido a la herencia de clases (pero que nunca será lo mismo que la herencia de clases).

Os dejo unas referencias para que os entretengais mientras.

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 “Pregunta: ¿Dónde están las clases en Javascript? (I)

  1. Pingback: Dónde están las clases en Javascript (II) « Newbe forever

    • Sí que lo es para los que venimos de lenguajes “clásicos”.
      Pero mola más de lo que parece en un principio. Comencé esta serie porque, en el que era entonces mi trabajo estábamos empezando la transición. Y teníamos largas discusiones acerca de lo que había que profundizar en el entendimiento del lenguaje si queríamos llegar a desarrollar en javascript aplicaciones como las que desarrollábamos en AS3.
      Por suerte o por desgracia, en el último año he tenido que cambiar de proyectos y lenguajes unas cuantas veces y no he tenido tiempo de sentarme a terminar esta incipiente serie de artículos.
      Quizás en otra vida, porque aunque parece que vuelvo a pisar tierra firme y tendré tiempo de volver a escribir, lo que ahora más me apetece es colaborar con el proyecto HaXe, lenguaje potente y versátil donde los haya, pero apenas documentado y menos aún en castellano.

      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