¿Beneficios de la herencia prototípica sobre la clásica?

271

Así que finalmente dejé de arrastrar mis pies todos estos años y decidí aprender JavaScript "correctamente". Uno de los elementos más deslumbrantes del diseño de idiomas es su implementación de herencia. Teniendo experiencia en Ruby, estaba realmente feliz de ver cierres y tipeos dinámicos; pero por mi vida no puedo entender qué beneficios se obtienen de las instancias de objetos que usan otras instancias por herencia.

Pierreten
fuente
posible duplicado de la comprensión de la herencia de prototipos en JavaScript
John Slegers

Respuestas:

560

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):

  1. Es sencillo.
  2. Es de gran alcance.
  3. Conduce a un código más pequeño y menos redundante.
  4. 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:

  1. El patrón prototípico de la herencia prototípica.
  2. 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:

  1. Crea un nuevo objeto.
  2. 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.createfunció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 circley 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 createfunción crea un clon circle, le asigna un nuevo radiusy 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 newpalabra clave une el thispuntero dentro del constructor a un clon del prototypedel 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 circleque 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:

  1. Una forma de crear un nuevo objeto (por ejemplo, literales de objeto).
  2. 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:

  1. Clases
  2. Objeto.
  3. Interfaces
  4. Clases abstractas.
  5. Clases finales
  6. Clases base virtuales.
  7. Constructores.
  8. 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 NullPointerExceptions 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:

  1. Variables privadas
  2. 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 :

  1. Delegación o herencia diferencial
  2. 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, _.extendhace exactamente esto.

Por supuesto, muchos programadores no consideran que esto sea una verdadera herencia porque instanceofy isPrototypeOfdicen 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á.

Aadit M Shah
fuente
33
Si bien entiendo de dónde vienes, y estoy de acuerdo en que la herencia prototípica es muy útil, creo que al suponer que "la herencia prototípica es mejor que la herencia clásica", ya estás destinado a fracasar. Para darle una perspectiva, mi biblioteca jTypes es una biblioteca de herencia clásica para JavaScript. Entonces, siendo un tipo que se tomó el tiempo para hacer eso, todavía me sentaré aquí y diré que la herencia de prototipos es increíble y muy útil. Pero es simplemente una de las muchas herramientas que tiene un programador. Todavía hay muchas desventajas para la herencia de prototipos también.
línea roja el
77
Estoy completamente de acuerdo con lo que dijiste. Siento que muchos programadores rechazan JavaScript por su falta de herencia clásica, o lo denuncian como un lenguaje simple y tonto. De hecho, es un concepto extremadamente poderoso, estoy de acuerdo, y muchos programadores deberían aceptarlo y aprenderlo. Dicho esto, también siento que hay un número significativo de desarrolladores en JavaScript que se oponen a cualquier forma de herencia clásica en JavaScript, cuando realmente no tienen ninguna base para sus argumentos. Ambos son igualmente poderosos por derecho propio e igualmente útiles también.
línea roja el
9
Bueno, esa es su opinión, pero continuaré en desacuerdo, y creo que la creciente popularidad de cosas como CoffeeScript y TypeScript muestran que hay una gran comunidad de desarrolladores que desearían utilizar esta funcionalidad cuando sea apropiado. Como dijiste, ES6 agrega azúcar sintáctico, pero aún no ofrece la amplitud de jTypes. En una nota al margen, no soy el responsable de su voto negativo. Si bien no estoy de acuerdo con usted, no creo que constituya que tenga una mala respuesta. Fuiste muy minucioso.
línea roja el
25
Estás usando mucho la palabra clonar , lo cual es simplemente incorrecto. Object.createestá creando un nuevo objeto con el prototipo especificado. Su elección de palabras da la impresión de que el prototipo se clona.
Pavel Horal
77
@Adit: realmente no hay necesidad de estar tan a la defensiva. Su respuesta es muy detallada y merece votos. No estaba sugiriendo que "vinculado" debería ser un reemplazo directo de "clon", sino que describe más apropiadamente la conexión entre un objeto y el prototipo del que hereda, ya sea que afirme su propia definición del término "clon". " o no. Cámbielo o no, es completamente su elección.
Andy E
42

Permítame responder la pregunta en línea.

La herencia del prototipo tiene las siguientes virtudes:

  1. Se adapta mejor a los lenguajes dinámicos porque la herencia es tan dinámica como el entorno en el que se encuentra. (La aplicabilidad a JavaScript debería ser obvia aquí). Esto le permite hacer cosas rápidamente sobre la marcha, como personalizar clases sin grandes cantidades de código de infraestructura. .
  2. Es más fácil implementar un esquema de prototipos de objetos que los esquemas clásicos de dicotomía clase / objeto.
  3. Elimina la necesidad de los bordes afilados complejos alrededor del modelo de objetos como "metaclases" (nunca metaclases me gustó ... ¡lo siento!) O "valores propios" o similares.

Sin embargo, tiene las siguientes desventajas:

  1. La verificación de tipos de un lenguaje prototipo no es imposible, pero es muy, muy difícil. La mayoría de las "verificaciones de tipos" de lenguajes prototípicos son verificaciones de estilo de "escritura de pato" en tiempo de ejecución puro. Esto no es adecuado para todos los entornos.
  2. Es igualmente difícil hacer cosas como optimizar el envío de métodos mediante análisis estático (o, a menudo, incluso dinámico). Se puede (subrayo: puede ) ser muy ineficiente con mucha facilidad.
  3. Del mismo modo, la creación de objetos puede ser (y generalmente es) mucho más lenta en un lenguaje de creación de prototipos que en un esquema de dicotomía clase / objeto más convencional.

Creo que puede leer entre las líneas anteriores y encontrar las ventajas y desventajas correspondientes de los esquemas tradicionales de clase / objeto. Por supuesto, hay más en cada área, así que dejaré el resto a otras personas que respondan.

SOLO MI OPINIÓN correcta
fuente
1
Oye, una respuesta concisa que no es fanboy. Realmente desearía que esta fuera la mejor respuesta para la pregunta.
siliconrockstar
Tenemos compiladores dinámicos Just-in-time hoy que pueden compilar el código mientras se ejecuta el código creando diferentes piezas de código para cada sección. JavaScript es en realidad más rápido que Ruby o Python que usan clases clásicas debido a esto, incluso si usa prototipos porque se ha trabajado mucho para optimizarlo.
aoeu256
28

En mi opinión, el principal beneficio de la herencia de prototipos es su simplicidad.

La naturaleza prototípica del lenguaje puede confundir a las personas que son clásicamente formación , pero resulta que en realidad este es un concepto realmente simple y poderoso, la herencia diferencial .

No necesita hacer una clasificación , su código es más pequeño, menos redundante, los objetos heredan de otros objetos más generales.

Si piensas prototípicamente , pronto notará que no necesita clases ...

La herencia prototípica será mucho más popular en el futuro cercano, la especificación ECMAScript 5th Edition introdujo elObject.create método, que le permite producir una nueva instancia de objeto que hereda de otra de una manera muy simple:

var obj = Object.create(baseInstance);

Esta nueva versión del estándar está siendo implementada por todos los proveedores de navegadores, y creo que comenzaremos a ver una herencia de prototipos más pura ...

CMS
fuente
11
"su código es más pequeño, menos redundante ...", ¿por qué? He echado un vistazo al enlace de Wikipedia para "herencia diferencial" y no hay nada que respalde estas afirmaciones. ¿Por qué la herencia clásica daría como resultado un código más grande y más redundante?
Noel Abrahams
44
Exactamente, estoy de acuerdo con Noel. La herencia de prototipos es simplemente una forma de hacer un trabajo, pero eso no lo hace de la manera correcta. Las diferentes herramientas funcionarán de diferentes maneras para diferentes trabajos. La herencia prototípica tiene su lugar. Es un concepto extremadamente poderoso y simple. Dicho esto, la falta de soporte para la verdadera encapsulación y el polimorfismo coloca a JavaScript en una desventaja significativa. Estas metodologías han existido mucho más tiempo que JavaScript, y son sólidas en sus principios. Así que pensar que prototipa es "mejor" es simplemente una mentalidad equivocada.
línea roja el
1
Puede simular una herencia basada en clases utilizando una herencia basada en prototipos, pero no al revés. Ese podría ser un buen argumento. También veo la encapsulación más como una convención que una característica del lenguaje (por lo general, puede romper la encapsulación a través de la reflexión). Con respecto al polimorfismo: todo lo que gana es no tener que escribir algunas condiciones simples "si" al verificar los argumentos del método (y un poco de velocidad si el método objetivo se resuelve durante la compilación). No hay desventajas reales de JavaScript aquí.
Pavel Horal
Los prototipos son increíbles, en mi opinión. Estoy pensando en crear un lenguaje funcional como Haskell ... pero en lugar de crear abstracciones basaré todo en prototipos. En lugar de generalizar la suma y factorial para "doblar", debe utilizar el prototipo de la función de suma y reemplazar el + con * y 0 con 1 para hacer el producto. Explicaré "Mónadas" como prototipos que reemplazan "entonces" de promesas / devoluciones de llamada con flatMap como sinónimo de "entonces". Creo que los prototipos pueden ayudar a llevar la programación funcional a las masas.
aoeu256
11

Realmente no hay mucho para elegir entre los dos métodos. La idea básica a entender es que cuando se le da al motor de JavaScript la propiedad de un objeto para leer, primero verifica la instancia y, si falta esa propiedad, verifica la cadena del prototipo. Aquí hay un ejemplo que muestra la diferencia entre prototipado y clásico:

Prototipo

var single = { status: "Single" },
    princeWilliam = Object.create(single),
    cliffRichard = Object.create(single);

console.log(Object.keys(princeWilliam).length); // 0
console.log(Object.keys(cliffRichard).length); // 0

// Marriage event occurs
princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1 (New instance property)
console.log(Object.keys(cliffRichard).length); // 0 (Still refers to prototype)

Clásico con métodos de instancia (ineficiente porque cada instancia almacena su propia propiedad)

function Single() {
    this.status = "Single";
}

var princeWilliam = new Single(),
    cliffRichard = new Single();

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 1

Clásico eficiente

function Single() {
}

Single.prototype.status = "Single";

var princeWilliam = new Single(),
    cliffRichard = new Single();

princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 0
console.log(cliffRichard.status); // "Single"

Como puede ver, dado que es posible manipular el prototipo de "clases" declaradas en el estilo clásico, no hay realmente ningún beneficio en el uso de la herencia prototípica. Es un subconjunto del método clásico.

Noel Abrahams
fuente
2
Mirando otras respuestas y recursos sobre este tema, su respuesta parecería estar diciendo: "La herencia de prototipos es un subconjunto del azúcar sintáctico agregado a JavaScript para permitir la aparición de la herencia clásica". El OP parece estar preguntando sobre los beneficios de la herencia prototípica en JS sobre la herencia clásica en otros idiomas en lugar de comparar técnicas de creación de instancias dentro de JavaScript.
Un fader oscuro
2

Desarrollo web: herencia prototípica versus herencia clásica

http://chamnapchhorn.blogspot.com/2009/05/prototypal-inheritance-vs-classical.html

Herencia prototípica Vs clásica - code q & a resuelto

Herencia prototípica Vs clásica

andrajoso
fuente
20
Creo que es mejor resumir el contenido de los enlaces en lugar de pegar el enlace (algo que yo solía hacer), a menos que sea otro enlace SO. Esto se debe a que los enlaces / sitios se caen y pierde la respuesta a la pregunta, y potencialmente afecta los resultados de búsqueda.
James Westgate
El primer enlace no responde a la pregunta de por qué la herencia prototípica? Simplemente lo describe.
viebel