Anteriormente comenté como se conseguía la herencia en Javascript haciendo uso de la cadena de prototipos. En este artículo indagaré un poco mas en ello y veremos como hacer uso ella.

¿Qué son los prototipos?

Un prototipo no es más que un objeto al que recurre otro objeto cuando no sabe cómo hacer algo que se le pide. Todos los objetos en Javascript poseen un enlace a un prototipo mediante el atributo privado __proto__. La cadena irá delegando en prototipos superiores hasta llegar al objeto primario Object que lo define el propio lenguaje. Si por algún motivo no existe en la cadena el atributo o método que llamamos, se devolverá undefined o se lanzará una excepción reespectivamente.

¿Cómo se usan?

Hay dos formas de usarlos. La forma de hacerlo radica en si se usa un función a modo de constructor o como factory. Yo recomiendo usar siempre que se pueda una factory, pero es conveniente explicar ambas maneras para saber el por qué.

Opción A - ¿Que hay de new, viejo?

Una función es un objeto. Por tanto a una función le podemos asignar propiedades o métodos. ¿En qué se diferencia de un objeto entonces? En que a las funciones se les puede asignar un prototipo. Si a esto le sumamos el operador new obtenemos una copia de esa función enlazada al prototipo. De momento todo suena complejo pero vamos a explicarlo mediante un caso práctico.

Queremos definir un objeto Dog (el uso de cánidos va a ser habitual ;P) del que hereden los objetos max y tom. Primero crearemos el prototipo, que tendrá un método común a todas las clases que hereden de él.

const dogProto = { bark() { console.log('Guau!') } };  

Ahora usaremos una función como objeto y le asignaremos dicho prototipo. La función acepta un parámetro name que si os fijáis lo que hace es asignarse a si misma dicha propiedad (hemos dicho anteriomente que las funciones son objetos con propiedades). Esto es lo que comúnmente se conoce como constructor, es decir, una función que cambia las propiedades de un objeto al ser creado, en este caso ella misma.

function Dog(name) {  
  this.name = name;
}

Dog.prototype = dogProto;  

Para obtener nuestros objetos max y tom usaremos el operador new.

const max = new Dog('Max');  
const tom = new Dog('Tom');

max.name   // 'Max'  
tom.bark() // 'Guau!'  

El operador new devuelve un nuevo objeto que es usado como contexto, el this dentro de la función, y al que se le asigna el protopipo de la misma. Cuando asignamos this.name realmente lo estamos haciendo sobre un nuevo objeto y no sobre la propia función. La función hace de constructor y modifica dicho objeto. Por tanto ahora tenemos dos nuevos objetos tom y max enlazados al prototipo dogProto mediante la propiedad privada __proto__, cada uno con el atributo propio name.

Por norma general, las funciones que deben hacer uso del new se escriben con la primera letra mayúscula. Es una forma de que quien lea nuestro código sepa que debe usarse de ese modo y no como una función corriente. Si olvidamos poner el new lo que pasará es que se usará como this el contexto global, haciendo que las cosas no vayan como esperamos (mas abajo explico por qué).

Opción B - Quería un objeto, refresco grande y patatas. Gracias.

La otra manera de conseguir la herencia es hacer uso de un factory. Un factory no es más que una función que se encarga de devolver un objeto que le pedimos. No necesitamos new ni modificar una función. Para ello haremos uso de la función Object.create. Esta función acepta como parámetro un objeto, devolviendo un nuevo objeto que lo usa como prototipo. A modo de ejemplo usaremos el prototipo anterior.

function dog(name) {  
  const myDog = Object.create(dogProto);
  myDog.name = name;
  return myDog;
}

const max = dog('Max');

max.name   // 'Max'  
max.bark() // 'Guau!'  

El resultado es el mismo de la Opción A. Las funciones factory se escriben en minúscula para indicar que no se use el operador new.

¿Constructor o factory?

Si el resultado es el mismo, ¿por qué usar una u otra? Ya he explicado que hacer uso del new puede llevarnos a errores si olvidamos ponerlo. ¿Que pasaría en el siguiente ejemplo? Nótese las mayúsculas (constructor).

const max = Dog('Max');  
console.log(max.name);  

TypeError: Cannot read property 'name' of undefined  

Si no usamos new no estamos devolviendo ningún objeto, por tanto no podemos obtener las propiedades de algo que está undefined. Claro, es que la función no devuelve nada. Tienes que hacer que se devuelva a sí misma, haz que devuelva this. Ok, vamos a remediarlo.

function Dog(name) {  
  this.name = name;
  return this;
}

const tom = Dog('Tom');  
console.log(tom.name);  // 'Tom'  

Ahora parece solucionado. ¿Ves? No era tan complicado. Vamos a crearnos varios objetos y ver si todo va como debe.

const max = Dog('Max');  
console.log(max.name);  // 'Max'

const tom = Dog('Tom');  
console.log(tom.name);  // 'Tom'

console.log(max.name);  // 'Tom'  
console.log(name);      // 'Tom'  

No es lo que esperabas ¿verdad? Si no hacemos uso del new se usará siempre el contexto global, es decir, el ámbito externo al que se ejecuta la función. ¿Por qué hay una variable name si no la he definido? Porque la función Dog modifica this añadiendo la propiedad name. En este caso this sería el mismo contexto en el que hemos definido max y tom, por tanto siempre que llamamos a Dog estamos modificando la variable global name o creándola si no existe. Sin embargo como vemos no hay errores que nos indiquen que algo va mal, porque realmente el código no los tiene y es correcto, pero obtenemos un comportamiento que no es el esperado.

¿Es malo entonces usar constructores? Sí y no.

Se debe evitar recaer en el uso de constructores siempre que podamos a menos que controlemos su ejecución. ¿Cómo hacerlo? Con funciones factory que se encarguen de hacerlo por nosotros. Por ejemplo podemos resolver el anterior ejemplo usando una función que se asegure de usar el new.

function dog(name) {  
  return new Dog(name);
}

Esto de hecho es lo que hace internamente Object.create, ahorrándonos tener que crear funciones constructoras a las que establecerle un prototipo.

¿Opción C? Clases en ES6

Si habéis llegado hasta aquí y conocéis la sintaxis de ES6 os estaréis preguntando porque no he hablado de la nueva definición de clases que introduce. Por dos motivos: realmente no son clases y condicionan a escribir de una manera que en Javascript es innecesaria.

¿Cómo que no son clases? ¡Pero si tienen su palabras reservadas class y extends! ¡Y su constructor y todo! Cierto, pero si le cambiamos el collar a un perro sigue siendo un perro ¿no? Parecerá mas bonito y educado, pero puede mordernos los zapatos igualmente.

Las clases en ES6 no son más que una sintaxis para que resulte más comoda la definición de funciones constructoras. No estamos definiendo un nuevo tipo como hacemos en Java o Python, ya que al final lo que obtenemos es un objeto con propiedades enlazado a un prototipo, es decir, lo mismo que teníamos en la Opción A de antes. Simplemente es una forma de hacer bonita y clara la manera de hacerlo. Podríamos definir una manera de obtener objetos Dog de la siguiente manera.

class Dog {

  constructor(name) {
    this.name = name;
  }

  bark() {
    console.log('Guau!');
  }
}

const max = new Dog('Max')  
max.name   // 'Max'  
max.bark() // 'Guau!'  

¿Por qué no usarlo entonces si es mas claro y sencillo? Realmente no existe problema en usarlo siempre que sepamos lo que hay debajo y estemos prevenidos de lo que puede conllevar un uso excesivo de la herencia. En el próximo capítulo de la serie veremos una alternativa a la herencia y por qué es mejor usar funciones factory.

"La primera regla del Club de la Lucha: No hablar del Club de la Lucha" - The Fight Club (1999)