Saltar al contenido

Blog técnico | Entendiendo los prototipos de JavaScript

¿Nunca has oído hablar de los prototipos en JavaScript? ¿Has oído hablar de ellos pero aún no los entiendes? O quizás los entiendas bastante bien pero hay algunas cosas que te despistan. Espero que este post ayude a aclarar algunos de los misterios que rodean a lo que son los prototipos en JavaScript y por qué debería importarte. Si no estás familiarizado en absoluto con los prototipos, entran en juego cuando empiezas a trabajar con objetos y herencias en JavaScript.

Entonces, ¿por qué debería importarme?

Para entender la herencia en JavaScript, primero hay que entender los prototipos. Los prototipos son la forma en que la herencia se hace en JavaScript y entra en juego en casi todo nuestro código JavaScript, nos demos cuenta o no. Por ejemplo, incluso una simple matriz en JavaScript utiliza prototipos. Si no entiendes los prototipos es fácil cometer algunos errores simples pero críticos. Esto es especialmente cierto si planeas usar conceptos orientados a objetos como la herencia en tu propio código. Y, aunque se necesita un poco de pensamiento guiado para entender realmente los prototipos, una vez que los consigas te darás cuenta de que no son tan complejos. Así que también podríamos saltar y comprender realmente de qué se trata.

Blog técnico | Entendiendo los prototipos de JavaScript
Blog técnico | Entendiendo los prototipos de JavaScript

Funciones del constructor

Las funciones de constructor se usan como clases en los lenguajes orientados a objetos. Y de hecho, si usas clases en JavaScript, esencialmente usan funciones constructoras entre bastidores. Todo lo que vamos a hablar en este post es cierto para la creación de objetos usando clases también, pero el uso de funciones constructoras en este post nos ayudará a entender mejor los prototipos.

Por lo tanto, considere la siguiente función del constructor:

función Gato(nombre, color) { este.nombre = nombre; este.color = color; } Cat.prototipo.age = 0; let fluffy = new Cat("Fluffy", "White");

En este código, Gato es una función constructora. Una función constructora no es, en realidad, diferente de cualquier otra función.De hecho, el término «función constructora» es sólo una nomenclatura común que sugiere que la función será llamada con la nueva palabra clave y será usada para crear propiedades y métodos en el objeto que se está creando (representado por esto).Cuando decimos, dejemos fluffy = nuevo Cat(«Fluffy», «White») esto es lo que sucede:1. El nuevo operador crea un nuevo objeto vacío.1. La función constructora se llama con el contexto de este conjunto al nuevo objeto vacío.1. Por lo tanto, dentro de la función, este.nombre = nombre; es lo mismo que decir fluffy.nombre = nombre. Esperemos que esto ayude a aclarar que las funciones constructoras no están haciendo nada mágico, son sólo funciones normales que están creando propiedades sobre esto – aunque las usemos de forma similar a como funcionan las clases en otros idiomas. De hecho, usando la nueva palabra clave, se puede crear un objeto a partir de cualquier función (simplemente no tiene sentido con la mayoría de las funciones).

¿Edad.prototipo.de.gato? ¿De qué se trata?

Ok, primero que nada, note que Cat.prototype.age no es realmente parte de la función Gato. También es útil darse cuenta cuando este código es ejecutado. Ver el código agrupado de esa manera podría engañarlo para que piense que Cat.prototype.age = 0; está siendo llamado cuando se crea un nuevo gato, lo que por supuesto, en una inspección más cercana no es cierto. Normalmente este código (incluyendo la creación de la función Gato sobre él) se llama cuando una página web o un módulo de JavaScript se carga por primera vez; haciendo así que la función Gato esté disponible para ser llamada.Así que piensa en lo que está ocurriendo allí. La función Gato está siendo creada y luego la propiedad age está siendo creada (con un valor de 0) en el prototipo de la función Gato. Todo esto ocurre mucho antes de que incluso creemos cualquier instancia de un gato; es todo un andamiaje.

Espera, ¿las funciones tienen un prototipo? ¿Qué es un prototipo?

Las funciones pueden ser usadas para crear funcionalidad de clase en JavaScript; y todas las funciones tienen una propiedad prototipo. Esa propiedad prototipo es en realidad una instancia de un objeto en memoria y cada función en JavaScript tiene una tanto si la usas como si no. Cada función (función constructora o no) tiene una instancia de objeto prototipo colgando de ella, interesante, ¿no? Ese objeto prototipo vacío se crea y se almacena en la memoria tan pronto como se declara la función.

Cuando se crea un nuevo objeto utilizando la nueva palabra clave, éste crea el nuevo objeto, lo pasa como tal a la función constructora (dejando que esa función le haga lo que quiera) y luego, y ésta es la parte importante, toma la instancia del objeto a la que apunta la propiedad prototipo de esa función y la asigna como el prototipo para ese objeto recién instanciado. El nuevo objeto se crea, y el objeto prototipo separado que está asociado con la función constructora se asigna como el prototipo para el nuevo objeto.

¿Así que los objetos también tienen una propiedad de prototipo?

Bueno, sí, pero no se accede de la misma manera. Los objetos tienen un prototipo pero no tienen una propiedad de prototipo. Sobre todo porque es una mala idea meterse con el prototipo de un objeto directamente. Así que las funciones tienen prototipos y los objetos tienen prototipos. Y, son muy similares, de hecho, el prototipo de una función y el prototipo de un objeto suelen apuntar al mismo objeto en la memoria.

Aquí está la principal diferencia: El prototipo de un objeto es el objeto en memoria (no una función o clase) del que hereda propiedades, lo que difiere del prototipo de una función que se utiliza como el objeto a asignar como prototipo para los nuevos objetos creados mediante esta función constructora . El prototipo de un objeto se puede recuperar llamando a myObject. Esta propiedad no está recomendada en la mayoría de los navegadores, pero no es una propiedad estándar y podría ser eliminada en futuras versiones de los navegadores. Si quieres acceder a ella, es mejor hacerlo llamando a Object.getPrototypeOf(myObject). Dicho esto, usaré __proto__ en adelante para simplificar.

Así que vamos a dar una definición real de cada uno de estos términos:

  • El prototipo de una función: El prototipo de una función es la instancia del objeto que se convertirá en el prototipo de todos los objetos creados usando esta función como constructor.
  • El prototipo de un objeto: El prototipo de un objeto es la instancia del objeto de la que se hereda.

Volver al código

Toda esa explicación puede ser confusa sin mostrar algunos ejemplos, así que vamos a sumergirnos. Si inspeccionas el prototipo del gato en nuestro ejemplo, obtienes el siguiente resultado:

<pre>fluffy.__proto__; // Cat {age: 0}</pre>

Nota al margen: Utilicé la consola de desarrollo de Chrome para ejecutar estas declaraciones y mostraré los resultados mostrados por Chrome como comentarios (siguiendo //) como se muestra arriba.

Si miramos más profundamente, podemos ver que en realidad hay una cadena de prototipos que fue creada. Nuestro nuevo objeto esponjoso hereda de nuestro prototipo de Gato, pero ese prototipo en realidad hereda de Objeto.

<pre>fluffy.__proto__; // Cat {age: 0} fluffy.__proto__.__proto__; // Objeto { }</pre>

Esto es casi siempre cierto; casi todos los objetos heredan, eventualmente, de Objeto. Y además, todas las cadenas de herencia terminan finalmente con nulo – lo que significa que has llegado al final de la cadena de herencia:

<pre>mullido.__proto__.__proto__.__proto__; // null</pre>

Observa que Fluffy tiene un prototipo (__proto__) de Cat.En realidad, decirlo así no es realmente exacto.Cat es una función, por lo que no puede ser un prototipo; recuerda en las definiciones anteriores que un prototipo no es una función sino una instancia de un objeto.Esto es importante recordarlo cuando se piensa en los prototipos – el prototipo de un objeto no es la función que lo creó sino una instancia de un objeto que está asociada a esa función.Puesto que esto es importante, explorémoslo un poco más:

Gato; // función Gato(nombre, color) { // este.nombre = nombre; // este.color = color; // } Prototipo de gato; // Gato {age: 0} esponjoso; // Gato {nombre: "esponjoso", color: "blanco", edad: 0}

Mira la diferencia entre Gato, Cat.prototipo y fluffy. Lo que esto muestra es que Gato es una función pero Cat.prototipo y fluffy son objetos. Muestra además que Cat.prototipo tiene una propiedad de edad (puesta a 0) y fluffy tiene tres propiedades – incluyendo la edad… lo cual no es realmente cierto (mantente en sintonía). Cuando defines una función, crea más que la función, también crea un nuevo objeto con esa función como su tipo y asigna ese nuevo objeto a la propiedad del prototipo de la función. Cuando creamos por primera vez la función Cat, antes de ejecutar la línea Cat.prototype.age = 0, si hubiésemos inspeccionado el prototipo de Cat se habría visto así: Cat {}, un objeto sin propiedades, pero de tipo Cat. Fue sólo después de que llamamos Cat.prototype.age = 0; que se vio así: Gato {age: 0}.

Propiedades heredadas vs. propiedades nativas

En el párrafo anterior, eludí el hecho de que Fluffy no tenía en realidad 3 propiedades como parece. Eso es porque la edad no es realmente una propiedad directa de Fluffy. Puedes ver esto ejecutando estas declaraciones:

fluffy.hasOwnProperty("nombre"); // true fluffy.hasOwnProperty("color"); // true fluffy.hasOwnProperty("edad"); // false

Esto se debe a que la edad pertenece en realidad al prototipo de fluffy; y sin embargo, si ejecuto la declaración fluffy.age;, en efecto devuelve 0.Lo que sucede en realidad aquí, es que cuando pedimos fluffy.age, comprueba si fluffy tiene una propiedad denominada age, y si la tiene la devuelve, si no, pregunta al prototipo de fluffy si tiene una propiedad de edad. Continúa haciendo esto a lo largo de toda la cadena del prototipo hasta que encuentra la propiedad correspondiente o encuentra un objeto con un prototipo nulo y si no encuentra la propiedad en la cadena del prototipo lo devolverá indefinido. Pero, si en el camino, encuentra una propiedad de edad, se detiene y devuelve ese valor.

¿Así que eso es el prototipo de encadenamiento?

Sip. Es posible que haya oído hablar antes de la cadena de prototipos. Es realmente muy sencillo de entender ahora que (esperemos) entienda un poco más sobre cómo funcionan los prototipos. Una cadena de prototipos es básicamente una lista enlazada de objetos que apuntan hacia atrás al objeto del que cada uno hereda.

Cambiar el prototipo de una función

Recuerde que el prototipo de una función es sólo un objeto, así que ¿qué pasaría si empezamos a cambiar las propiedades del prototipo de una función después de crear objetos a partir de ella? Consideremos los siguientes ejemplos:

función Gato(nombre, color) { este.nombre = nombre; este.color = color; } Cat.prototype.age = 3; var fluffy = new Cat("Fluffy", "White"); var scratchy = new Cat("Scratchy", "Black"); fluffy.age; // 3 scratchy.age; // 3 Cat.prototype.age = 4; fluffy.age; // 4 scratchy.age; // 4

Así pues, observe que al cambiar la edad de la propiedad prototipo de la función Gato también cambió la edad de los gatos que habían heredado de ella.Esto se debe a que cuando se creó la función Gato, también lo hizo su objeto prototipo; y cada objeto que heredó de ella heredó esta instancia del objeto prototipo como su prototipo.Ahora considere el siguiente ejemplo que realmente cambia el prototipo de la función Gato para que apunte a un objeto completamente nuevo:

pre… función Gato(nombre, color) { este.nombre = nombre; este.color = color; } Cat.prototipo.age = 3; var fluffy = nuevo Cat("Fluffy", "Blanco"); var scratchy = nuevo Cat("Scratchy", "Negro"); fluffy.age; // 3 scratchy. age; // 3 Cat.prototype = { age: 4 }; fluffy.age; // 3 scratchy.age; // 3 var muffin = new Cat("Muffin", "Brown"); muffin.age; // 4</pre

En primer lugar, esta es la línea de código importante en ese ejemplo: Cat.prototype = { age: 4 };.Note que no sólo cambié el valor de la propiedad prototype.age a 4, sino que también cambié el prototipo de la función Gato para que apunte a un nuevo objeto. Así que mientras que Muffin heredó el nuevo objeto prototipo, los prototipos de Fluffy y Scratchy siguen apuntando a su objeto prototipo original, que originalmente heredaron de la función Gato. Esto ilustra que la propiedad prototipo de una función es la instancia de objeto que se convertirá en el prototipo (o proto ) para los objetos creados usando esta función como constructor. Puede que quieras estudiar este código durante un minuto y volver a leer este párrafo para comprender realmente lo que está pasando.

Un ejemplo más para ilustrar este punto. ¿Qué pasaría si cambiara el valor de la propiedad de la edad del prototipo de Fluffy? ¿Sería esto diferente que simplemente cambiar la edad de Fluffy? Sí, sería diferente. Piensa en los ejemplos anteriores, y en cómo la edad no es en realidad una propiedad de fluffy, sino una propiedad del prototipo de fluffy (proto).

Así que, dada esta configuración:

pre… función Gato(nombre, color) { este.nombre = nombre; este.color = color; } Cat.prototipo.age = 3; var fluffy = nuevo Cat("Fluffy", "Blanco"); var scratchy = nuevo Cat("Scratchy", "Negro");</pre>

Compare este ejemplo:

…fluffy.age = 4; fluffy.age; // 4 scratchy.age; // 3</pre

Por este ejemplo:

…fluffy.__proto__.age = 4; fluffy.age; // 4 scratchy.age; // 4</pre

Esto produce resultados diferentes porque en el primer ejemplo, sólo estamos añadiendo una propiedad de nueva edad a fluffy.__proto__ tiene propiedades de edad con los valles 4 y 3, respectivamente. Cuando se pide fluffy.age, encuentra la propiedad en el objeto fluffy, por lo que lo devuelve inmediatamente sin tener que buscar en la cadena del prototipo.

Mientras que en el segundo ejemplo, fluffy todavía no tiene su propia propiedad de edad, pero su prototipo (que es el mismo caso en la memoria que el prototipo de scratchy) tiene ahora el nuevo valor 4, lo que afecta tanto a la edad de fluffy como a la de scratchy.

Herencia múltiple

Debo confesar que no es algo que utilice a menudo porque tiendo a preferir la composición a la herencia; pero eso no quiere decir que no tenga lugar. Digamos que quieres crear una función constructora que "herede" a partir de otra función constructora, de forma muy parecida a como lo harías en otros lenguajes orientados a objetos cuando creas una clase que hereda de otra.Veamos un ejemplo de cómo puedes hacer esto:

Prefunción Animal(nombre) { this.name = nombre; } Animal.prototipo.edad=1; función Gato(nombre, color) { este.nombre = nombre; } Cat.prototipo.= nuevo Animal(null); var fluffy = nuevo Cat("Fluffy", "Blanco"); fluffy.name; // Fluffy fluffy.color; // Blanco fluffy.age; // 1 fluffy. hasOwnProperty("nombre"); // true fluffy.hasOwnProperty("color"); // true fluffy.hasOwnProperty("edad"); // false</pre

Nótese que la edad es la única propiedad que no es una propiedad directa de fluffy.Esto es porque cuando llamamos a la función constructora Gato, ésta pasó en nuestro nuevo objeto (fluffy/this) a la función Animal que creó la propiedad de nombre en el objeto.la función Gato también añadió la propiedad de color a fluffy/this.pero la propiedad de edad se añadió al prototipo de la función Animal, nunca se añadió directamente a fluffy.

Funciones hereditarias

En todos los ejemplos anteriores, usé propiedades como el nombre, el color y la edad para ilustrar los objetos y la herencia.Sin embargo, todo lo que he hecho anteriormente con las propiedades se puede hacer con las funciones.Si creáramos una función speak() en la función Gato de esta manera: Cat.prototype.speak = function() { alert($0027meow$0027); };, esa función sería heredada por todos los objetos que tienen a Cat como prototipo, al igual que con las propiedades de nombre, color y edad.

Conclusión

Si eres como yo, hará falta experimentar para que todo esto se haga realidad. Te recomiendo que dupliques los ejemplos anteriores por tu cuenta y que te entretengas con ellos para ver qué puedes hacer y qué te sorprende. Los prototipos, una vez que los tienes, no son tan complejos, pero para mí, al menos, me costó un poco jugar con el código antes de entenderlos.

Categorías: technicalTags: javascript, deep dive