Sé que esta respuesta se retrasa 3 años, pero realmente creo que las respuestas actuales no proporcionan suficiente información sobre cómo la herencia prototípica es mejor que la herencia clásica .
Primero, veamos los argumentos más comunes que los programadores de JavaScript afirman en defensa de la herencia de prototipos (estoy tomando estos argumentos del conjunto actual de respuestas):
- Es sencillo.
- Es de gran alcance.
- Conduce a un código más pequeño y menos redundante.
- Es dinámico y, por lo tanto, es mejor para lenguajes dinámicos.
Ahora todos estos argumentos son válidos, pero nadie se ha molestado en explicar por qué. Es como decirle a un niño que estudiar matemáticas es importante. Claro que sí, pero al niño ciertamente no le importa; y no puedes hacer que un niño sea matemático diciendo que es importante.
Creo que el problema con la herencia de prototipos es que se explica desde la perspectiva de JavaScript. Me encanta JavaScript, pero la herencia de prototipos en JavaScript está mal. A diferencia de la herencia clásica, hay dos patrones de herencia prototípica:
- El patrón prototípico de la herencia prototípica.
- El patrón constructor de la herencia prototípica.
Desafortunadamente, JavaScript utiliza el patrón constructor de la herencia prototípica. Esto se debe a que cuando se creó JavaScript, Brendan Eich (el creador de JS) quería que se pareciera a Java (que tiene herencia clásica):
Y lo estábamos empujando como un hermano pequeño de Java, ya que un lenguaje complementario como Visual Basic era para C ++ en las familias de idiomas de Microsoft en ese momento.
Esto es malo porque cuando las personas usan constructores en JavaScript piensan en constructores que heredan de otros constructores. Esto está mal. En la herencia prototípica, los objetos heredan de otros objetos. Los constructores nunca entran en escena. Esto es lo que confunde a la mayoría de las personas.
Las personas de lenguajes como Java, que tiene herencia clásica, se confunden aún más porque, aunque los constructores parecen clases, no se comportan como clases. Como Douglas Crockford declaró:
Esta indirección tenía la intención de hacer que el lenguaje pareciera más familiar para los programadores entrenados de manera clásica, pero no pudo hacerlo, como podemos ver por la muy baja opinión que los programadores Java tienen de JavaScript. El patrón de construcción de JavaScript no atrajo a la multitud clásica. También oscureció la verdadera naturaleza prototípica de JavaScript. Como resultado, hay muy pocos programadores que saben cómo usar el lenguaje de manera efectiva.
Ahí tienes. Directo de la boca del caballo.
Verdadera herencia prototípica
La herencia de prototipos se trata de objetos. Los objetos heredan propiedades de otros objetos. Eso es todo al respecto. Hay dos formas de crear objetos utilizando la herencia de prototipos:
- Crea un nuevo objeto.
- Clonar un objeto existente y extenderlo.
Nota: JavaScript ofrece dos formas de clonar un objeto: delegación y concatenación . De ahora en adelante, usaré la palabra "clonar" para referirme exclusivamente a la herencia por delegación, y la palabra "copiar" para referirme exclusivamente a la herencia por concatenación.
Basta de hablar. Veamos algunos ejemplos. Digamos que tengo un círculo de radio 5
:
var circle = {
radius: 5
};
Podemos calcular el área y la circunferencia del círculo a partir de su radio:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
Ahora quiero crear otro círculo de radio 10
. Una forma de hacer esto sería:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
Sin embargo, JavaScript proporciona una mejor manera: delegación . La Object.create
función se usa para hacer esto:
var circle2 = Object.create(circle);
circle2.radius = 10;
Eso es todo. Acabas de hacer una herencia prototípica en JavaScript. ¿No fue eso simple? Coges un objeto, lo clonas, cambias lo que necesites y, bueno, listo: tienes un objeto nuevo.
Ahora puede preguntar: "¿Cómo es esto simple? Cada vez que quiero crear un nuevo círculo necesito clonar circle
y asignarle un radio manualmente". Bueno, la solución es utilizar una función para hacer el trabajo pesado por usted:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
De hecho, puede combinar todo esto en un único objeto literal de la siguiente manera:
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
Herencia de prototipos en JavaScript
Si observa en el programa anterior, la create
función crea un clon circle
, le asigna un nuevo radius
y luego lo devuelve. Esto es exactamente lo que hace un constructor en JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
El patrón constructor en JavaScript es el patrón prototipo invertido. En lugar de crear un objeto, crea un constructor. La new
palabra clave une el this
puntero dentro del constructor a un clon del prototype
del constructor.
¿Suena confuso? Es porque el patrón constructor en JavaScript complica innecesariamente las cosas. Esto es lo que la mayoría de los programadores encuentran difícil de entender.
En lugar de pensar en objetos que heredan de otros objetos, piensan en constructores que heredan de otros constructores y luego se confunden por completo.
Hay muchas otras razones por las que se debe evitar el patrón de constructor en JavaScript. Puedes leer sobre ellos en mi blog aquí: Constructores vs Prototipos
Entonces, ¿cuáles son los beneficios de la herencia prototípica sobre la herencia clásica? Veamos nuevamente los argumentos más comunes y expliquemos por qué .
1. La herencia prototípica es simple
CMS afirma en su respuesta:
En mi opinión, el principal beneficio de la herencia prototípica es su simplicidad.
Consideremos lo que acabamos de hacer. Creamos un objeto circle
que tenía un radio de 5
. Luego lo clonamos y le dimos al clon un radio de 10
.
Por lo tanto, solo necesitamos dos cosas para que la herencia de prototipos funcione:
- Una forma de crear un nuevo objeto (por ejemplo, literales de objeto).
- Una forma de extender un objeto existente (por ejemplo
Object.create
).
En contraste, la herencia clásica es mucho más complicada. En herencia clásica tienes:
- Clases
- Objeto.
- Interfaces
- Clases abstractas.
- Clases finales
- Clases base virtuales.
- Constructores.
- Destructores
Tienes la idea. El punto es que la herencia prototípica es más fácil de entender, más fácil de implementar y más fácil de razonar.
Como Steve Yegge lo pone en su clásica publicación de blog " Retrato de un N00b ":
Los metadatos son cualquier tipo de descripción o modelo de otra cosa. Los comentarios en su código son solo una descripción del cálculo en lenguaje natural. Lo que hace que los metadatos sean metadatos es que no es estrictamente necesario. Si tengo un perro con algunos documentos de pedigrí y pierdo el papeleo, todavía tengo un perro perfectamente válido.
En el mismo sentido, las clases son solo metadatos. Las clases no son estrictamente necesarias para la herencia. Sin embargo, algunas personas (generalmente n00bs) encuentran que las clases son más cómodas para trabajar. Les da una falsa sensación de seguridad.
Bueno, también sabemos que los tipos estáticos son solo metadatos. Son un tipo de comentario especializado dirigido a dos tipos de lectores: programadores y compiladores. Los tipos estáticos cuentan una historia sobre el cómputo, presumiblemente para ayudar a ambos grupos de lectores a comprender la intención del programa. Pero los tipos estáticos pueden desecharse en tiempo de ejecución, porque al final son solo comentarios estilizados. Son como el papeleo de pedigrí: podría hacer que cierto tipo de personalidad insegura sea más feliz con su perro, pero al perro ciertamente no le importa.
Como dije anteriormente, las clases dan a las personas una falsa sensación de seguridad. Por ejemplo, obtienes demasiados NullPointerException
s en Java incluso cuando tu código es perfectamente legible. Creo que la herencia clásica generalmente se interpone en la programación, pero tal vez eso sea solo Java. Python tiene un sorprendente sistema clásico de herencia.
2. La herencia prototípica es poderosa
La mayoría de los programadores que provienen de un contexto clásico argumentan que la herencia clásica es más poderosa que la herencia prototípica porque tiene:
- Variables privadas
- Herencia múltiple.
Este reclamo es falso. Ya sabemos que JavaScript admite variables privadas a través de cierres , pero ¿qué pasa con la herencia múltiple? Los objetos en JavaScript solo tienen un prototipo.
La verdad es que la herencia de prototipos admite la herencia de múltiples prototipos. La herencia de prototipos simplemente significa que un objeto hereda de otro objeto. En realidad, hay dos formas de implementar la herencia prototípica :
- Delegación o herencia diferencial
- Clonación o herencia concatenativa
Sí, JavaScript solo permite que los objetos deleguen en otro objeto. Sin embargo, le permite copiar las propiedades de un número arbitrario de objetos. Por ejemplo, _.extend
hace exactamente esto.
Por supuesto, muchos programadores no consideran que esto sea una verdadera herencia porque instanceof
y isPrototypeOf
dicen lo contrario. Sin embargo, esto puede remediarse fácilmente almacenando una serie de prototipos en cada objeto que hereda de un prototipo mediante concatenación:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
Por lo tanto, la herencia prototípica es tan poderosa como la herencia clásica. De hecho, es mucho más poderoso que la herencia clásica porque en la herencia prototípica puede elegir qué propiedades copiar y qué propiedades omitir de diferentes prototipos.
En la herencia clásica es imposible (o al menos muy difícil) elegir qué propiedades desea heredar. Utilizan clases e interfaces virtuales para resolver el problema del diamante .
Sin embargo, en JavaScript es muy probable que nunca escuche sobre el problema del diamante porque puede controlar exactamente qué propiedades desea heredar y de qué prototipos.
3. La herencia prototípica es menos redundante
Este punto es un poco más difícil de explicar porque la herencia clásica no necesariamente conduce a un código más redundante. De hecho, la herencia, ya sea clásica o prototípica, se usa para reducir la redundancia en el código.
Un argumento podría ser que la mayoría de los lenguajes de programación con herencia clásica están tipados estáticamente y requieren que el usuario declare explícitamente los tipos (a diferencia de Haskell, que tiene tipeo estático implícito). Por lo tanto, esto conduce a un código más detallado.
Java es conocido por este comportamiento. Recuerdo claramente a Bob Nystrom mencionando la siguiente anécdota en su publicación de blog sobre Pratt Parsers :
Tienes que amar el nivel de burocracia de Java "por favor firme en cuadruplicado" aquí.
Nuevamente, creo que eso es solo porque Java apesta mucho.
Un argumento válido es que no todos los idiomas que tienen herencia clásica admiten herencia múltiple. Nuevamente, Java me viene a la mente. Sí, Java tiene interfaces, pero eso no es suficiente. A veces realmente necesitas herencia múltiple.
Dado que la herencia prototípica permite la herencia múltiple, el código que requiere herencia múltiple es menos redundante si se escribe usando la herencia prototípica en lugar de en un lenguaje que tiene herencia clásica pero no herencia múltiple.
4. La herencia prototípica es dinámica
Una de las ventajas más importantes de la herencia de prototipos es que puede agregar nuevas propiedades a los prototipos después de su creación. Esto le permite agregar nuevos métodos a un prototipo que estarán disponibles automáticamente para todos los objetos que deleguen en ese prototipo.
Esto no es posible en la herencia clásica porque una vez que se crea una clase no se puede modificar en tiempo de ejecución. Esta es probablemente la mayor ventaja de la herencia prototípica sobre la herencia clásica, y debería haber estado en la parte superior. Sin embargo, me gusta guardar lo mejor para el final.
Conclusión
La herencia prototípica es importante. Es importante educar a los programadores de JavaScript sobre por qué abandonar el patrón constructor de la herencia prototípica en favor del patrón prototípico de la herencia prototípica.
Necesitamos comenzar a enseñar JavaScript correctamente y eso significa mostrar a los nuevos programadores cómo escribir código usando el patrón prototípico en lugar del patrón constructor.
No solo será más fácil explicar la herencia prototípica utilizando el patrón prototípico, sino que también será un mejor programador.
Si le gustó esta respuesta, también debería leer la publicación de mi blog sobre " Por qué es importante la herencia de prototipos ". Créame, no se decepcionará.