Herencia de JavaScript desde el punto de vista de un nerd aburrido: Fábrica de constructores

lámpara de luz y manzana de discordia Esta es una historia sobre una pieza muy especial de JavaScript, el lenguaje artificial más utilizado en el mundo hoy (2019).

El artículo presenta una especie de visión filosófica de la herencia en JavaScript, y solo me atrevo a esperar que se base en la fuente de conocimiento más impresionante: la vida misma en todas sus manifestaciones. No sé si esto fue una inspiración al crear el diseño de la cadena prototipo en JavaScript.

Pero si es así, entonces es tan significativo y poderoso que cuando empiezo a pensar en ello, a veces se hace aún más difícil respirar ...

(todos los enlaces están subrayados )


¡También estoy seguro de que ninguno de nosotros dudará de que Brendan Ike (Eich), el autor del lenguaje de programación JavaScript, es un genio excepcional! Y no solo porque a menudo repite:
¡Apuesta siempre por JavaScript!
¡Empecemos! Y nuestro primer punto de partida será la Imaginación, en la que primero apagaremos todos los prejuicios, omisiones y otros efectos secundarios.

Nos dirigimos de regreso al futuro , la era que precedió a la creación de Internet moderna a principios de la década de 1990.

Desde la época de los primeros hackers que inventaron todo lo que nosotros ( los empleados de TI ) estamos usando ahora, nos hemos movido a un bosquejo impresionante: sobre la guerra entre los navegadores Netstcape Navigator 2 e Internet Explorer 3. Java acaba de salir, y casi todo, desde Internet moderno aún no se ha inventado o No pre-abierto. Es posible que, como yo, en esos "buenos viejos tiempos" eras joven, y pudieras recordar este maravilloso sentimiento de participación en todo el esplendor que se está creando ante tus ojos.

Por lo tanto, tiene una PC muy poderosa en el Intell Pentium 200 MMX más moderno con 32Mb de RAM, Windows 3.11 o incluso Windows 95 y ¡mira hacia el futuro con esperanza! Y, por supuesto, ambos navegadores también están instalados. Usted tiene un acceso telefónico , a través del cual se conecta a la red para recibir nueva basura, también, para estudiar o simplemente chatear, chatear. Aunque, espere un momento, aún no puede chatear directamente en el navegador, lo más probable es que esté utilizando sistemas de entrega de mensajes retrasados, algo como EMail o quizás UseNet , o, muy posiblemente, ya haya dominado la entrega instantánea a través de IRC .

Pasan un par de años y literalmente TODO cambia ... De repente ves la animación de los copos de nieve en las páginas web felicitándote por Año Nuevo y Navidad. Por supuesto, le interesa cómo se hace esto y descubre un nuevo lenguaje: JavaScript. Como el HTML no es nuevo para usted, comienza a aprender este atractivo techno-craft. Pronto descubres CSS y resulta que esto también es importante, ya que todo está hecho de una combinación de estos tres: HTML, JavaSript y CSS. Wow

Casi al mismo tiempo, es posible que note un par de cosas geniales en Windows, CScript y HTA aparecieron en él, e incluso entonces fue posible crear aplicaciones de escritorio completas directamente en JS (y esto todavía funciona).

Y así comenzó a hacer su primer servidor web, posiblemente en Perl o C ~ C ++ . Quizás incluso comenzaste a usar sistemas operativos tipo Unix y lo hiciste en bash . Y todo ello "girando girando" gracias a la interfaz de puerta de enlace común (no lo confunda con ese otro CGI ). PHP todavía casi no existe, pero tal vez te guste pronto.

Era 200x. Ahora está haciendo ASP en JScript . Esto es muy similar al JavaScript que funciona dentro de sus páginas web. ¡Esto es genial! Está considerando crear su propio motor de plantillas , una especie de parodia de XML . Y luego, de repente, alguien llama a AJAX todas estas formas divertidas de cargar dinámicamente el contenido que ha estado utilizando durante varios años. Y todos ellos ahora piensan que solo hay XMLHTTPRequest , pero recuerda que los datos se pueden transferir a BMP , IFrame o incluso insertando la etiqueta <script> . Y, de repente, alguien habla con entusiasmo sobre JSON y lo útil que es, cuando has estado manejando datos durante una eternidad con algo como esto:

document.write("<" + "script src=" + path + ">"); 

Ahora no es todo lo que importa, pero aún puedes recordar cómo ...

Cuando recupera sus sentidos, de vez en cuando comienza a encontrarse con Rhino o Nashorn en un intento de satisfacer los deseos de los clientes de Java que utilizan Alfresco o Asterisk . Y ya has escuchado sobre el inminente advenimiento de JavaScript en el mundo de los microchips, y estas noticias te inspiran mucho. Y, por supuesto, ahora tienes jQuery y Backbone .

Al ver las nevadas del próximo 2010, ya sabes que en tu mundo todas las reglas del juego pronto cambiarán, ya que el "Jugador No. 1" ha entrado en el campo: Node.js. Y los próximos 10 años que pasarás con este nuevo juguete, e incluso ahora, en 2019, aún no puedes tener suficiente de lo genial que es.

En general, estás contento con todo, todo te conviene, todos estos juguetes y juegos en ellos constituyen una gran parte de los intereses de tu vida.

Pero hay una pequeña pregunta que te haces día tras día, noche tras noche durante dos décadas:

Si tuviera que hacer esto, ¿cómo explicaría Empathy usando JavaScript?


Usted sabe que uno de los temas más complicados en JavaScript es la herencia de prototipos y la cadena de prototipos . Y le encanta este tema, puede explicar cómo funciona y funciona todo, simplemente porque lo aprendió casi desde el principio, incluso antes de que naciera la primera versión de la Norma , y donde, como recordará, hay 4.2 .1 Objetos :
ECMAScript admite herencia basada en prototipos. Cada constructor tiene un prototipo asociado, y cada objeto creado por ese constructor tiene una referencia implícita al prototipo (llamado prototipo del objeto) asociado con su constructor. Además, un prototipo puede tener una referencia implícita no nula a su prototipo, y así sucesivamente; Esto se llama la cadena prototipo .
Cada objeto se crea con una referencia implícita al prototipo. Esto se llama la cadena de herencia prototipo, que puede continuar durante el tiempo que desee.

Wow ... Y si de repente, como yo, pudieras pensar que este es uno de los mejores inventos de Compuer Science , ¿cómo expresarías el efecto que la lectura de esta declaración tuvo en ti?

Volvamos al principio. En el patio de 1995. Eres Brendan Ike y necesitas inventar un nuevo lenguaje de programación. Quizás te guste Lisp o Scheme , al menos algunas de sus partes favoritas. Y se enfrenta a la necesidad de resolver el problema de la herencia , ya que debe pretender que el lenguaje tiene una cierta implementación de OOP . Pensemos : necesita mezclar todas las cosas que le gustan, tal vez también algunas que no le gustan, y el cóctel resultante debe ser lo suficientemente bueno como para que nadie note un truco hasta que haya una necesidad real de entender cómo funciona. arreglado por dentro.

Y ahora la pregunta es: ¿qué pasaría con la herencia?

Volvamos a la realidad por un momento. ¿Qué sabemos todos sobre la herencia? Algunas piezas obvias de respuestas a esta pregunta:

  1. La mayoría de las formas de vida se basan en el genoma . Es un depósito de información sobre las características probables y el supuesto comportamiento de los seres vivos. Cuando eres una criatura viviente, llevas una parte del genoma dentro de ti, puedes distribuirlo, pero lo recibiste de generaciones anteriores.
  2. Puedes crear una criatura viva usando dos técnicas: mezclar (genomas) de dos antepasados ​​o, posiblemente, usar la clonación monoica de uno de ellos. Por supuesto, hoy somos tecnologías disponibles que permiten mezclar los genomas de más de una criatura, pero esto es mucho menos obvio y no tan natural.
  3. El factor tiempo es importante. Si no hay una propiedad heredada, o no la hay, nuestra única salida es crearla desde cero. Además de esto, también existe el Legado, lo que pasa al ser de sus ancestros no a través del genoma, sino a través de las leyes de propiedad, y esto también puede ser significativo.

Volviendo al pasado, y la pregunta correcta que debemos hacernos ahora es: Y, de hecho, herencia ¿Qué queremos obtener?

Bueno, en cualquier caso, en el proceso de resolver el problema de la herencia, necesitamos al menos llenar el vacío entre la programación y la vida real, de lo contrario, en general, no tendremos derecho a llamarlo Herencia.

Ahora terminemos: estamos en la década de 1995, y tenemos una PC poderosa con solo 32 megabytes de RAM, y estamos tratando de crear un lenguaje interpretado, por lo que debemos cuidar mucho esta memoria. Cada pieza de datos, especialmente los objetos String , consume mucha memoria, y deberíamos poder declarar esta pieza solo una vez, y siempre que sea posible en el futuro, siempre usemos solo punteros al área de memoria ocupada por esta pieza. Resumimos: una pregunta muy difícil.

Existe una opinión popular: " JavaScript se crea a partir de objetos ". Usando esta trivialidad, podemos responder fácilmente a la pregunta " de qué " heredar y " qué ": de objetos a objetos. En aras de la cuestión de guardar memoria, resulta que todos los datos deben almacenarse en estos objetos, y todos estos enlaces de datos también deben almacenarse en las propiedades heredadas de estos objetos. Quizás ahora esté claro por qué en 1995 realmente necesitábamos crear un diseño basado en una cadena de prototipo: ¡ahorra memoria el mayor tiempo posible! Y en general, creo que esto todavía puede ser un aspecto muy importante.

Según el diseño indicado y la opinión " todo es un Objeto ", podemos intentar clonar algo así. Pero, ¿qué es la clonación aquí ahora? Creo que siguiendo los requisitos de nuestros requisitos, podemos asumir algo como Structural o Surface Copy , en algo similar a los predecesores de Object.assign moderno.
Implementemos una copia estructural simple en 1995 usando for (var i in) {} , ya que el estándar ya permitía esto :

 // back in 1995 cloning // it is not deep clone, // though we might not need deep at all var cloneProps = function (clonedObject, destinationObject) { for (var key in clonedObject) { destinationObject[key] = clonedObject[key]; } }; 

Como puede ver, este enfoque todavía "funciona", aunque en general, por supuesto, recomendaría mirar el módulo de extensión profunda para una comprensión más detallada de cómo hacer la clonación en JavaScript, pero para los fines del artículo, una aplicación consistente es muy adecuada para nosotros descrito por cloneProps , porque podríamos usarlo en aquellos tiempos antiguos:

  • clonando objetos usando el constructor: usando el constructor, cree al menos dos clones diferentes

     // cloneProps is described above var SomeConstructor = function (clonedObject) { cloneProps(clonedObject, this); }; var someExistingObjectToClone = { foo : 'bar' }; var clone1 = new SomeConstructor(someExistingObjectToClone); var clone2 = new SomeConstructor(someExistingObjectToClone); // clone1.foo == clone2.foo 
  • clonando un constructor de un constructor: implementamos el uso del comportamiento de un constructor de otro constructor

     var SomeConstructor = function () { this.a = 'cloned'; }; var AnotherConstructor = function () { // Function.prototype.call // was already invented in 1st ECMA-262 SomeConstructor.call(this); }; 
  • clonando un constructor usando un objeto: usaremos el mismo objeto para implementar la clonación en al menos dos constructores

     var existentObject = { foo : 'bar' }; var SomeConstructor = function () { cloneProps(foo, this); }; var OtherConstructor = function () { cloneProps(foo, this); }; 
  • Clonando un Objeto de otro Objeto: use un objeto para crear varios de sus clones . No hay nada que describir aquí, solo tómalo como es nuestro cloneProps del primer ejemplo anterior.

Con la clonación, en general, todo es simple, como vemos, todo está claro y en general, pero ...

¿Es tan fácil para nosotros hacer la Herencia de Entidades, usando una combinación de sus predecesores?

  • Herencia de un objeto usando el Constructor: este es el propósito de los diseñadores, solo mostraremos cómo se diseñó originalmente .

     var existentObject = { foo : 'bar' }; var SomeConstructor = function () {}; SomeConstructor.prototype = existentObject; var inheritedObject = new SomeConstructor(); // we have no instanceof yet in ECMA 262 of 1995 // therefore we are unable to rely on this window.alert(inheritedObject.foo); // bar 
  • Herencia del Constructor de otro Constructor: sin duda, el primero que notó esto fue un Genio sobresaliente. Con todo, este es otro ejemplo clásico de todas partes .

     var FirstConstructor = function () { this.foo = 'bar'; }; var InheritedConstructor = function () { FirstConstructor.call(this); }; InheritedConstructor.prototype = { bar : 'foo' }; InheritedConstructor.prototype.constructor = FirstConstructor; var inherited = new InheritedConstructor(); // { foo : 'bar', bar : 'foo' } 

    sería posible decir algo sofisticado, pero ¿por qué?
  • Herencia del Constructor del Objeto: de nuevo, solo usamos .prototype = object cada vez que lo necesitamos, no hay nada que describir, siempre necesitamos asignar Constructor.prototype ya que se supone que coloca el objeto allí, y mediante un enlace implícito obtenemos todas sus propiedades .
  • Heredar un objeto de un objeto: lo mismo. Acabamos de poner el primer objeto en Constructor.prototype y tan pronto como digamos nuevo Constructor crearemos una copia heredada en la que habrá referencias implícitas a las propiedades de nuestro primer objeto.

Y, por supuesto, en todas estas situaciones con herencia, tendremos la oportunidad de verificar usando la instancia de qué constructor creamos los objetos, aunque, por supuesto, debe tenerse en cuenta que la instancia de sí mismo apareció en los estándares casi cuatro años después.

Es cierto que quedaba un detalle tan pequeño del párrafo 4.2.1:
ser capaz de hacer esto por el tiempo que sea necesario, como dice:
y así sucesivamente
Bueno, tratemos de hacer que la herencia sea realmente interminable , utilizando la tecnología de 1995 .

De hecho, imaginemos que tenemos dos entidades y no constructores, sino objetos simples. Y queríamos heredar uno del otro, y luego quizás del otro, y del otro, y así sucesivamente ...

Pero como?


Echa un vistazo un poco más lejos, más profundo.
La respuesta correcta aquí nuevamente es: ¿ Herencia de lo que necesitamos crear?

Después de todo, no necesitamos estas Entidades por nuestra cuenta. Necesitamos sus propiedades: memoria de consumo de datos asociada; y también, probablemente necesitemos algún comportamiento: métodos que usen estos datos. Y será igual de honesto , si tenemos la oportunidad de verificar dónde y dónde heredamos y usar qué. En general, sería genial si pudiéramos reproducir el diseño inherente de los patrones de herencia en el futuro, lo que implica que si heredamos uno del otro muchas veces, siempre obtendremos el mismo resultado, de acuerdo con lo que escribimos (contrato) . Aunque todavía es así, puede ser tan útil para nosotros arreglar de alguna manera el momento de la creación, ya que nuestras entidades "anteriores" pueden cambiar con el tiempo, y los "herederos", que les respetan en esto, aún cambian con ellos. puede que no quiera hacerlo.

Y, dado que todo nuestro código es una combinación de datos y comportamiento, ¿será generalmente normal usar el mismo método, combinando datos y presentación, al diseñar un sistema de herencia?

En cuanto a mí, todo esto se parece a lo que vemos al observar la Vida en todas sus formas increíbles. ¡Desde el primer unicelular, hasta el multicelular y sus descendientes, y hasta los animales, las personas, el humanismo y las tribus, las civilizaciones, el intelecto y sus formas artificiales, y más allá del espacio, la galaxia y las estrellas! y:
"... Todo lo que tenemos que hacer es asegurarnos de seguir hablando ..."
(todo lo que necesitamos hacer es continuar la comunicación)

Increíble en su consideración, una cita de Stephen Hawking , posteriormente popularizada en esta obra maestra de Pind Floyd .

Los lenguajes de programación basados ​​en el paso de mensajes y un concepto basado en Flow implementado a través de una API interna sólida le permiten pasar de datos simples a abstracciones más altas, descripción y todo lo demás. Creo que esto es puro arte, y cómo funciona en particular en estructuras JavaScript profundamente ocultas a través de relaciones implícitas entre datos en cadenas de prototipos.

Imagine nuevamente a dos antepasados, se comunican y en un momento sus emociones y sentimientos crean un hijo. El niño crece, conoce a otro niño, se comunican, y aparece el siguiente descendiente, y más y más y más ... Y siempre necesitamos dos padres, de lo contrario no es natural, ya será ingeniería genética. Dos, ni más ni menos. Un descendiente recibe lo mismo que su Legado, por lo que es simple y comprensible.

Entiendo que sonará extraño, pero sí, tenemos todo lo que necesitamos para crear este modelo de Herencia en 1995. Y la base de todo esto es precisamente 4.2.1 Objetos , referencia implícita a través de prototipo.

Y eso es exactamente, tal como es, combinando ParentObject con ParentConstructor especificando .prototype y luego Constructor probablemente nos creará un ChildObject , por supuesto, si decimos la palabra mágica " nuevo ":

 var ParentObject = { foo : 'bar' }; var ParentConstructor = function () {}; ParentConstructor.prototype = ParentObject; var ChildObject = new ParentConstructor(); // starting from 1995 and then ECMA 262 // we are able to say new // each time we need a ChildObject 

Podemos discernir aquí a nuestros dos antepasados. En el momento en que dijimos la palabra mágica " nuevo " les pedimos que chatearan. Si no quieren comunicarse, Life se detendrá, el proceso caerá con un error y el compilador (intérprete) nos lo informará.

Por supuesto, sí, pero pedimos el árbol de herencia o dejamos que sea obviamente mucho más simple, al menos para el árbol genealógico . Y la respuesta sigue siendo la misma ... nuestro objeto hijo crece y se convierte en un objeto padre , luego se encuentra con un nuevo objeto constructor y tan pronto como decimos la codiciada palabra " nuevo " - magia:

 // this Assignment is just to show it grew up var ChildObjectGrownToParent = ChildObject; var AnotherConstructor = function () {}; AnotherConstructor.prototype = ChildObjectGrownToParent; var SequentialChildObject = new AnotherConstructor(); // checking Life Cycle ;^) console.log(ChildObject instanceof ParentConstructor); // true console.log(SequentialChildObject instanceof ParentConstructor); // true console.log(SequentialChildObject instanceof AnotherConstructor); // true 

Y podemos continuar haciendo esto hasta el infinito. Y, tal vez, realmente creo que esta fue la idea principal al desarrollar el diseño de la cadena de prototipos, porque como todos sabemos, este enfoque crea algunos problemas muy claros pero no menos desagradables ...

1: Comunidad ... Como puedes comprobar por ti mismo, especificando en .prototype ParentConstructor ' a u AnotherConstructor' a es un Contrato social muy serio y estricto en nuestra Tribu. Crea una referencia a las propiedades ParentObject ( .foo ) para los Herederos: ChildObject y SequentialChildObject . Y si nos deshacemos de esta indicación, estos enlaces desaparecerán. Si ideamos y reasignamos esta referencia a algún otro objeto, nuestros herederos heredarán instantáneamente otras propiedades. Por lo tanto, combinando antepasados ​​a través de .prototype , probablemente podríamos decir que estamos creando algún tipo de célula de la sociedad , porque estos "antepasados" pueden reproducir muchos descendientes idénticos cada vez que les preguntamos acerca de esto usando nuevos . Y así, habiendo destruido la "familia", estamos arruinando las cualidades hereditarias de sus descendientes, tal drama; ^)

Tal vez todo esto es hablar de Legacy en nuestro código, ¡deberíamos ocuparnos de esto cuando creamos un código seguro y compatible ! Por supuesto, en 1995 no se discutieron SOLIDOS , el Principio de Liskov y el Diseño por Contrato y GRASP , pero es obvio que estas metodologías no se crearon "desde cero", todo comenzó mucho antes.

2: Familia ... Podemos verificar fácilmente que nuestro ParentObject puede ser muy frívolo con otros Constructos. Esto no es justo, pero podemos usar tantos Constructores como queramos en la Herencia de nuestro ParentObject y así crear tantas familias como queramos. Por otro lado, cada Constructor está muy asociado con ParentObject a través de la tarea .prototype, y si no deseamos dañar a nuestros herederos, debemos mantener esta conexión el mayor tiempo posible. Podríamos llamarlo el arte de la tragedia en la historia de nuestra tribu. Aunque, por supuesto, esto nos protege de la amnesia: el olvido de lo que heredamos y de quién, y por qué nuestros herederos obtienen tal Legado . Y, alabando al gran Mnemosyne !, podemos realmente probar fácilmente nuestro Prototype Chain Tree y encontrar artefactos de lo que hicimos mal.

3: Vejez ... Nuestro ParentObject y Constructor son ciertamente susceptibles a daños durante el ciclo de vida de nuestro programa. Podemos tratar de solucionar esto, pero nadie está a salvo de los errores. Y todos estos cambios pueden dañar a los herederos. Debemos cuidar las pérdidas de memoria . Por supuesto, podemos destruir partes innecesarias del código en tiempo de ejecución y liberar memoria que ya no se usa en nuestro Ciclo de vida . Además, debemos deshacernos de todas las posibilidades de crear paradojas temporales en las cadenas de prototipos, ya que de hecho es bastante simple heredar el ancestro de su propio descendiente. Pero esto ya puede ser muy peligroso, ya que tales técnicas de coquetear con el pasado del futuro pueden crear montones completos de Heisenbags difíciles de reproducir, especialmente si tratamos de medir algo que puede cambiar con el tiempo.

Crónica de decisiones


Que sea simple, obvio y no muy útil, pero en lugar de pensar en nuestro Constructor y ParentObject como mamá y papá, describámoslos como un huevo y ... polen . Luego, en el futuro, cuando creamos el cigoto usando la palabra atesorada " nuevo ", ¡ya no dañará nuestra imaginación!

En el momento en que hagamos esto, inmediatamente nos libraremos de los tres problemas anteriores. Por supuesto, para esto necesitamos la capacidad de crear los cigotos por sí mismos, lo que significa que necesitamos la Fábrica de Diseñadores. Y ahora llámenlo como quieran, madres, padres, qué diferencia hace, porque la conclusión es que si vamos a decir " nuevo ", entonces debemos crear una jaula de diseñador de flores "nueva", ponerle polen, y solo esto nos permitirá para cultivar un nuevo Snowdrop "correcto" en el lejano y nevado 2020m:

 var Pollen = { season : 'Spring' }; // factory of constructors var FlowersFactory = function (proto) { var FlowerEggCell = function (sort) { this.sort = sort; }; FlowerEggCell.prototype = proto; return FlowerEggCell; }; var FlowerZygote = FlowersFactory(Pollen); var galanthus = new FlowerZygote('Galanthus'); 

(y sí, no te olvides de verificar la temporada con este snowdrop, de lo contrario los copos de nieve caerían o caerían, y un snowdrop sería una flor de primavera ...)

Por supuesto, la Complejidad Ciclomática de las decisiones que intentas crear usando este enfoque será bastante comparable al Riddle Einstein . Por lo tanto, aquí "preparé" una biblioteca , puede ayudar con la creación de cadenas de diseñadores y memorización (nota del editor: bueno, takoe, toma un pastel de un estante, bla-bla-bla ) ...

Y aunque no puedo probarlo, este enfoque se ha utilizado con bastante éxito de vez en cuando durante dos décadas, si necesita estar 146% seguro de que todo es normal con la herencia. Puede ver fácilmente por sí mismo que se prueba, reproduce y admite de manera elemental (Nota del editor : sí, en este momento, dejó todo y fue a asegurarse ).

Por supuesto, esta no es toda la historia, simplemente declaramos el hecho: JavaScript está diseñado lo suficientemente bien como para describir el Gráfico Genealógico directamente a través de la Herencia. Por supuesto, aquí no tocamos astutamente el tema de la degradación de la Clase , pero estoy seguro de que usted mismo puede reemplazar fácilmente FlowerEggCell con FlowerEggCellClass dentro de FlowersFactory: la esencia seguirá siendo la misma si desea verificar sus flores a través de la instancia , verá que todos son descendientes de FlowerEggCell a los que se refiere a través de FlowerZygote . Y, por supuesto, ahora puede cambiar las propiedades del propio FlowerZygote , ya que esto no hará ningún daño al FlowersFactory en sí mismo , seguirá siendo capaz de crear otros nuevos constructores FlowerEggCell o FlowerEggCellClass en el futuro de acuerdo con el diseño original de " referencia " que haya puesto allí.

Espero que este artículo haya disipado todas las dudas sobre la importancia de la palabra .prototipo la próxima vez que vea nulo this , . call (null , . apply (null . bind (null code style (. .: Sorrow , , , ).

!

!

V

Source: https://habr.com/ru/post/470994/


All Articles