Mozilla's getting a new look. What do you think? https://mzl.la/brandsurvey

Herencia y la cadena de prototipos

Esta traducción está incompleta. Por favor, ayuda a traducir este artículo del inglés.

JavaScript es un poco confuso para desarrolladores experimentados en lenguajes basados en clases (como Java o C++), al ser dinámico y no proporcionar una implementación de clases en sí misma (la palabra clave class se introdujo en ES6, pero sólo como azúcar sintáctico, ya que JavaScript sigue estando basado en prototipos).

En lo que a herencia se refiere, JavaScript sólo tiene una estructura: objetos. Cada objeto tiene un enlace interno a otro objeto: su prototipo. Ese objeto prototipo tiene su propio prototipo, y así sucesivamente hasta que se alcanza un objeto cuyo prototipo es null. Por definición, null no tiene prototipo, y actúa como el final de esta cadena de prototipos.

A pesar de que a menudo se considera como una de las principales debilidades de JavaScript, el modelo de herencia de prototipos es de hecho más potente que el modelo clásico. Por ejemplo, es bastante simple construir un modelo clásico a partir de un modelo de prototipos, mientras que a la inversa es una tarea mucho más difícil.

Herencia con la cadena de prototipos

Heredando propiedades

Los objetos JavaScript son "bolsas" dinámicas de propiedades que tienen un enlace a un objeto prototipo. Cuando intentamos acceder a una propiedad de un objeto, la propiedad no sólo se buscará en el propio objeto sino también en el prototipo del objeto, en el prototipo del prototipo, y así sucesivamente hasta que se encuentre una propiedad que coincida con el nombre o se alcance el final de la cadena de prototipos.

Siguiendo el estándar ECMAScript, la notación algún Objeto.[[Prototype]] se usa para designar el prototipo de algún objeto. Esto es equivalente a la propiedad JavaScript __proto__ (en desuso). Desde ECMAScript 5, [[Protoype]] se accede utilizando Object.getPrototypeOf() y Object.setPrototypeOf().

Esto es lo que ocurre cuando intentamos acceder a una propiedad:

// Supongamos que tenemos un objeto o, con propiedades a y b:
// {a: 1, b: 2}
// o.[[Prototype]] tiene propiedades b y c:
// {b: 3, c: 4}
// Finalmente, o.[[Prototype]].[[Prototype]] es null.
// Este es el final de la cadena de prototipos, ya que null,
// por definición, no tiene [[Prototype]].
// Por tanto, la cadena completa de prototipos es:
// {a:1, b:2} ---> {b:3, c:4} ---> null

console.log(o.a); // 1
// ¿Hay una propiedad 'a' en o? Sí, y su valor es 1.

console.log(o.b); // 2
// ¿Hay una propiedad 'b' en o? Sí, y su valor es 2.
// El prototipo también tiene una propiedad 'b', pero no se ha visitado. 
// Esto se llama "solapamiento de propiedades"

console.log(o.c); // 4
// ¿Hay una propiedad 'c' en o? No, comprobamos su prototipo.
// ¿Hay una propiedad 'c' en o.[[Prototype]]? Sí, y su valor es 4.

console.log(o.d); // undefined
// ¿Hay una propiedad 'd' en o? No, comprobamos su prototipo.
//  ¿Hay una propiedad 'd' en o.[[Prototype]]? No, comprobamos su prototipo.
// o.[[Prototype]].[[Prototype]] es null, paramos de buscar.
// No se encontró la propiedad, se devuelve undefined

Dar valor a una propiedad de un objeto crea una propiedad. La única excepción a las reglas de funcionamiento de obtener y dar valores ocurre cuando hay una propiedad heredada con un getter o un setter.

Heredando "métodos"

JavaScript no tiene "métodos" en la forma que los lenguajes basados en clases los define. En JavaScript, cualquier función puede añadirse a un objeto como una propiedad. Una función heredada se comporta como cualquier otra propiedad, viéndose afectada por el solapamiento de propiedades como se muestra anteriormente (siendo, en este caso, una especie de redefinición de métodos).

Cuando una función heredada se ejecuta, el valor de this apunta al objeto que hereda, no al prototipo en el que la función es una propiedad.

var o = {
  a: 2,
  m: function(b){
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// Cuando en este caso se llama a o.m, 'this' se refiere a o

var p = Object.create(o);
// p es un objeto que hereda de o

p.a = 12; // crea una propiedad 'a' en p
console.log(p.m()); // 13
// cuando se llama a p.m, 'this' se refiere a p.
// De esta manera, cuando p hereda la función m de o, 
// 'this.a' significa p.a, la propiedad 'a' de p

Maneras diferentes de crear objetos y la cadena de prototipos resultante

Objetos creados mediante estructuras sintácticas

var o = {a: 1};

// El objeto recién creado o tiene Object.prototype como su [[Prototype]]
// o no tiene ninguna propiedad llamada 'hasOwnProperty'
// hasOwnProperty es una propiedad propia de Object.prototype. 
// Entonces o hereda hasOwnProperty de Object.prototype
// Object.prototype es null como su prototype.
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// Arrays hereda de Array.prototype 
// (que tiene métodos como indexOf, forEach, etc.)
// La cadena de prototipados sería:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// Las funciones heredan de Function.prototype 
// (que tiene métodos como call, bind, etc.)
// f ---> Function.prototype ---> Object.prototype ---> null

Con un constructor

Un "constructor" en JavaScript es "solo" una función que pasa a ser llamada con el operador new.

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};

var g = new Graph();
// g es un objeto con las propiedades 'vértices' y 'edges'.
// g.[[Prototype]] es el valor de Graph.prototype cuando new Graph() es ejecutado.

Con Object.create

ECMAScript 5 Introdujo un nuevo método: Object.create(). Llamando este método creas un nuevo objeto. El prototype de este objeto es el primer argumento de la función:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (heredado)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); 
// undefined, por que d no hereda de Object.prototype

Con la palabra reservada class

ECMAScript 6 introduce un nuevo set de palabras reservadas implementando classes. aunque estos constructores lucen mas familiares para los desarrolladores de lenguajes basados en clases, Aun así no son clases. JavaScript permanece basados en prototipos. Los nuevos keywords incluyen class, constructor, static, extends, and super.

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

Performance

El tiempo de búsqueda para las propiedades que están en lo alto de la cadena de prototipo puede tener un impacto negativo en el rendimiento, y esto puede ser significativo en el código donde el rendimiento es crítico. Además, tratar de acceder a las propiedades inexistentes siempre atravesara la cadena de prototipos completamente.

También, cuando iteramos sobre las propiedades de un objeto, cada propiedad enumerable que se encuentra en la cadena de prototipo será enumerada.

Para comprobar si un objeto tiene una propiedad definida en sí mismo y no en alguna parte de su cadena de prototipo, Es necesario usar para esto el método  hasOwnProperty que todos los objetos heredan de Object.prototype.

hasOwnProperty es la única cosa en JavaScript que se ocupa de las propiedades y no atraviesa la cadena de prototipos.

Nota: Esto no es suficiente para chequear si una propiedad esta undefined. la propiedad podría existir, pero el valor justamente sucede que esta seteado como undefined.

Malas practicas: Extensión de prototipos nativos

Una mala característica que a menudo se usa, es extender Object.prototype o uno de los otros pre-incorporados prototypes.

Esta técnica se llama monkey patching y rompe la encapsulación. Si bien, es utilizado por librerías como Prototype.js, no hay una buena razón para saturar los tipos pre-incorporados con funcionalidades adicionales no estándar.

La única buena razón para extender los pre-incorporados prototipos es modificar las funcionalidades nuevas de los motores de JavaScript; por ejemplo:

Array.forEach, etc.

Ejemplo

B heredará de A:

function A(a){
  this.varA = a;
}

// Cual es el propósito de incluir varA en el prototipo si A.prototype.varA siempre va a ser la sombra de
// this.varA, dada la definición de la función A arriba?
A.prototype = {
  varA : null,  // No deberíamos atacar varA desde el prototipo como haciendo nada?
      // Tal vez intentando una optimización al asignar espacios ocultos en las clases? 
      // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instanciar variables
      // podría ser válido si varA no fuera inicializado únicamente por cada instancia.
  doSomething : function(){
    // ...
  }
}

function B(a, b){
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB : {
    value: null, 
    enumerable: true, 
    configurable: true, 
    writable: true 
  },
  doSomething : { 
    value: function(){ // override
      A.prototype.doSomething.apply(this, arguments); // call super
      // ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();

Las partes importantes son:

  • Los tipos son definidos en .prototype
  • Usar Object.create() para heredar

prototype y Object.getPrototypeOf

JavaScript es un poco confuso para desarrolladores que vienen de lenguajes como Java o C++, ya que todo es dinámico, en todo momento de la ejecución, y no tiene clases en lo absoluto. Todo es solamente instancias (objetos). Incluso las "clases" que creamos, son solo funciones (objetos).

Probablemente notaste que nuestra función A tiene una propiedad especial llamada prototype. Esta propiedad especial funciona con el operador de JavaScript new. La referencia al prototipo objeto es copiada al interno [[Prototype]] propiedad de la instancia new. Por ejemplo, cuando creas una variable var a1 = new A(), JavaScript (después de haber creado el objeto en memoria y antes de correr function A() con this definido a él) setea a1.[[Prototype]] = A.prototype. Cuando a continuación accedes a las propiedades de la instancia, JavaScript primero chequea si existen en el objeto directamente, y si no, mira en el [[Prototype]]. Esto significa que todo lo que definas en el prototipo es efectivamente compartido a todas las instancias, e incluso después puedes cambiar partes del prototipo y que todos los cambios se hagan en todas las instancias.

Si, en el ejemplo de arriba, pones var a1 = new A(); var a2 = new A(); entonces a1.doSomething se referiría a Object.getPrototypeOf(a1).doSomething, que seria lo mismo que A.prototype.doSomething que definiste, i.e. Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething.

resumiendo, prototype es para tipos, mientras que Object.getPrototypeOf() es lo mismo para instancias.

[[Prototype]] es visto como recursivo, i.e. a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething etc., hasta que se encuentra o Object.getPrototypeOf retornará null.

Entonces, cuando llamas

var o = new Foo();

JavaScript en realidad hace

var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);

(o algo similar) y cuando después haces

o.someProp;

chequea si o tiene una propiedad someProp. Si no, busca en Object.getPrototypeOf(o).someProp y si ahí no existe, busca en Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp y así sucesivamente.

En conclusión

Es esencial entender el modelo de prototipado por instancias antes de escribir código complejo que hace uso de esto. También, sé consciente del largo de la cadena de prototipado en tu código y romperlo si es necesario para evitar posibles problemas de rendimiento. Adicionalmente, el prototipo nativo nunca debería ser extendido a menos que esto sea por motivo de compatibilidad con nuevas versiones de JavaScript.

Etiquetas y colaboradores del documento

 Colaboradores en esta página: tavofigse, guumo, alagos, metalback, MartinIbarra, 10537, blacknack
 Última actualización por: tavofigse,