Prototype
Tranquilo, yo tampoco lo entendí a la primera, pero luego de leer por mucho rato te lo puedo explicar en idioma humano. Además, trataré de codificar en español, sé que no es buena práctica, pero para que lo podamos entender juntos, funciona para mí 🐱👤
Probablemente uno de los conceptos más temidos dentro de JavaScript pero a su vez, te prometo que luego de esta parte todo tendrá mucho más sentido.
🔤 ¿Qué es prototype?
En JavaScript, un prototipo es un objeto que sirve como plantilla para crear otros objetos. Cada objeto de JavaScript tiene un prototipo, que es otro objeto del que las futuras instancias hereda propiedades y métodos.
Aún no queda claro, ¿Cierto? Respira porque verás la matrix y tendrás una revelación divina, sígueme el hilo.
Ya sabemos cómo crear un objeto cierto? Para continuar vamos a realizar algunos ejemplos y usaré pokemones, ¿Por qué? Porque son geniales.
Bien, sabemos que podemos declarar un objeto vacío, y posteriormente, poder asignarles ciertos valores con ayuda del .
Veamos como se vería esto en código.
let pokemon = {}
pokemon.nombre = 'Pikachu'
pokemon.vida = 100
Nuestro pokemon ha de necesitar un nombre y vida para existir:)
Pero claro, un pokemon también tiene ciertas acciones, vamos añadirlas al pokemon también con ayuda del punto.
Listo, ahora tenemos un objeto creado con propiedades y métodos (Entendemos por métodos a las funciones declaradas dentro de un objeto).
Pero y ¿Ahora qué? Pikachu no es el único pokemon en toda la serie, existen otros muchos más, y para lograr ello, lo más óptimo sería crear una función constructora para a partir de esta, crear nuevas instancias, nuevos objetos, nuevos pokemones.
Pero oye, ¿Función constructora? ¿Nuevas instancias? No entiendo.
Tranquilo pequeño gran programador, sigue leyendo.
Una función constructora no es nada más que una función que nos ayudará a crear nuevos objetos. Y una instancia no es más que el objeto que vamos a crear a partir de esta función constructora.
¿Lo tienes? Sigamos.
A continuación entraremos ya en el tema, pero desde un punto básico y estaremos mejorando nuestro código a medida que avancemos, hasta llegar al contenido de __proto__
vs prototype
.
Nos quedamos en crear nuestra función constructora, ¿Cómo se vería esto en código?
Crearemos una función Pokemon
, tendrá como parámetro nombre, el cual la pasaremos como valor a la propiedad nombre (pokemon.nombre)
de nuestra futura instancia. El resto del código prácticamente es lo mismo:)
Felicidades, ya tenemos un constructor para crear pokemones! Ahora, toca crearlos 🤔 Para ello usaremos el constructor que acabamos de desarrollar 💡
Veamos que obtenemos en la consola si imprimimos los valores de dos instancias a partir de nuestra función Pokemon.
Obtenemos dos objetos muy parecidos, salvo por la diferencia en el valor de “nombre”
, pero mantienen el resto de propiedades iguales, incluyendo los métodos.
Si te das cuenta, al final de ambos puedes ver [[Prototype]]
, su significado lo veremos más adelante.
Pero pensemos un poco, en Pokemon no solo tenemos 2 pokemones, tenemos cientos, miles, y si creamos a todos ellos, las funciones caminar y salir se van a asignar en diferentes espacios de memoria pero al final van hacer lo mismo.
Puedes ver lo que ocurre gráficamente a continuación. Cada pokemon asignará sus métodos en diferentes espacios en memoria, y si tenemos miles, puedes darte la idea de lo colapsada que estará nuestro memory heap.
Veamos como podemos solucionar esto.
El problema está en que se están recreando las funciones de cada pokemon, qué pasaría si en vez de asignarlas a cada pokemon, hacemos que estos hagan referencia a un objeto que contenga todas estas funciones.
Pero, no habría conflictos? Yo también me pregunté lo mismo, pero no es algo que deberíamos preocuparnos, pues gracias a “this”
, estaremos haciendo referencia a cada instancia posible y no estará referenciando a un solo pokémon.
Veamos como se vería esto en código.
¿Qué hicimos? Bueno, creamos un objeto pokemonMethods
y le asignamos dos funciones, ahora dentro de nuestra función Pokemon, solo hacemos referencia a cada una de estas funciones para agregarlas como métodos de nuestra futura instancia.
Excelente! 😼 Ahora con este código, hemos dado solución a ese problema. Al ejecutar el programa, se creará pokemonMethods
solo una vez en memoria, y luego cada que creamos un nuevo pokemon, lo que estamos haciendo es una referencia a ese objeto de métodos en vez de recrearlos por cada uno.
Pero puedes pensar en algún problema que estamos teniendo actualmente en el código? Por si no te das cuenta, si tuviéramos muchos más métodos dentro de pokemonMethods
, tendríamos que hacer una referencia dentro de nuestra función Pokemon
por cada uno, y no sería algo muy óptimo, y la verdad que da mucha pereza. 😴
Así que sí, vamos a mejorar nuestro código.
🔨 Object.create()
Y esto? Querido amigo, este pedazo de código será la salvación.
Lo que queremos hacer es que nuestra función constructora Pokemon, siempre pueda hacer referencia a sus métodos que están dentro de pokemonMethods.
Y precisamente algo parecido es lo que hace nuestro object.create
Lo que hace esta función es en sí devolvernos un nuevo objeto. Y su trabajo es, darle la capacidad a ese nuevo objeto de poder acceder a propiedades y métodos de un “objeto padre”.
Puedes ver cómo se realiza la búsqueda de los valores a continuación.
Lo tienes más claro? La búsqueda de tal propiedad o método iniciará en el objeto en sí, y luego si no la encuentra, pasará a buscarla dentro del objeto que pasamos como argumento en Object.create()
Bellísimo, no es así? Ahora veamos cómo se vería esto dentro de nuestro ejemplo con pokemones.
Si pruebas el código verás que obtendrás los mismos resultados, nuestros pokemones instanciados tendrá la posibilidad de acceder a sus métodos! 😉
Aquí es donde empezará lo genial, tan genial como pueden llegar a ser los patrones de creación de instancias en JS. Listo? Sigamos. 💪🏻
Vamos a volvernos mucho más pros 🎩💎
El hecho de que declaremos los métodos fuera de nuestra función general que sería Pokemon
, no se ve bien verdad?, sería mucho más elegante si lo hacemos dentro de algo que tenga relación con este.
Aquí es donde entra en juego el prototype en JS.
Vi muchos recursos y realmente se enriendan mucho para un concepto tan sencillo, así que relájate.
Vamos a ir construyendo este concepto.
Lo único que es prototype en JS es una propiedad en una función. ¿Lo tienes?
Todas las funciones tienen un prototype. ¿Raro no?
Vamos a imprimir el valor de prototype en una función vacía.
Lo que nos devuelve la consola es un objeto con una propiedad “constructor
” dentro de él.
function randomFn(){
}
console.log(randomFn.prototype)
// {Constructor ƒ}
// -> constructor : ƒ randomFn()
// -> [[Prototype]]: Object
Y ese el concepto con el que te debes de quedar, si te preguntan en una entrevista, ¿Qué es prototype? Puedes responder: Es una propiedad que tienen todas las funciones que hace referencia a un objeto 🤖
Perfecto! Pero qué se supone que voy hacer con eso? 🤨 Pues aquí viene lo mágico. Al tenerla todas las funciones en JS, podemos usar esa propiedad para administrar ciertos valores. Y qué mejor que usarla para evitar administrar nuestros métodos de los pokemones fuera de ese contexto de Pokemon
pues poca relación mantenía con el mismo.
¿Cómo haríamos esto? Observemos a continuación.
Almacenamos nuestros métodos dentro de Pokemon.prototype
y pasaremos en el object.create la referencia al prototype de Pokemon
(Pokemon.prototype)
para que delegue la búsqueda hacia allí.
Aún no te quedó claro? No te preocupes, mira a prototype como una cajita, y podemos almacenar en ella lo queramos, pero mantendrá siempre ese vínculo con nuestro objeto dueño del prototype, en este caso Pokemon, y que estarán disponibles para futuras instancias. Veamos la siguiente representación
La lógica de búsqueda de propiedades será la misma.
Iniciará buscando en la instancia que vayamos a crear, luego en la función Pokemon y luego pasará a buscar dentro del Pokemon.prototype
A que sí era sencillo 🤗
Hagamos una pequeña recapitulación de lo que hemos aprendido hasta el momento. Sabemos cómo declarar una función constructora (Función que devuelve un objeto), además sabemos cómo añadir métodos al prototype de nuestra función constructora, y por último pero menos importante, aprendimos a usar object.create para delegar la búsqueda fallida al prototype de nuestra función constructora 😮
Su único propósito? Compartir ciertos métodos en todas las futuras instancias de un constructor en particular 🌟
Hemos tenido un magnífico avance, no es así? Sugiero te tomes una pequeña pausa para digerir lo anterior para continuar. Listo?
🔑 New keyword
Como te dije, aquí no dejaremos de mejorar nuestro código, y sí, sonaré molesto pero aún podemos hacerlo, pues debería haber una forma más fácil de hacer todo este proceso de crear nuevas instancias y bla bla bla, verdad? Echemos un vistazo a esta parte del código.
Esas dos líneas son fundamentales para nuestra función constructora, y probablemente ya hayas escuchado sobre la palabra clave new
, pues ahora veremos cómo podemos implementarla acá para optimizar todo.
La forma en que funciona es la siguiente: Puedes ver a new como decir, crear una nueva instancia de → Pokemon (Función constructora).
const Pikachu = new Pokemon('Pikachu');
Y ahora bien, ¿Cuál es la diferencia cuando usamos new? Pues si en esta también podemos crear instancias. Atento y mira a continuación
Lo que sucede es que detrás de cámaras, JS creará un objeto this, y le pasará como object.create al prototype de ese constructor actual. Ahora tú como desarrollador, para añadir métodos o propiedades, tendrás que hacer referencia a ese objeto this. Finalmente JS implícitamente hará un return de ese objeto this.
Increíble no es así? Veamos como se vería aplicado en nuestro objeto con pokemones.
Oh là là, mucho más elegante cierto? 🎩🍷
Recuerda que cuando usamos new, ya no es necesario crear ese objeto this ni devolverlo en el return, todo lo hace JS implícitamente.
Ahora sí! Lo que estábamos esperando, veamos el verdaderooo
🥊 __proto__
o [[Prototype]] vs prototype
Apuesto a que has visto __proto__
por algún lado y no tenías ni idea de lo que era cierto?. Mira el siguiente pedazo de código. Que por cierto __proto__
se le conoce también como dunder proto
Vamos a ver todo lo que nos imprime la consola si imprimimos el valor de una instancia con nuestra función constructora Pokemon.
const Pikachu = new PokemonConNew('Pikachu')
console.log(Pikachu)
A mí me aparece [[Prototype]] en vez de __proto__
porque tengo un tema instalado en mi devtools.
Se ve raro y feo, ¿No es así? Pero veamos cómo va indentado este output. Vemos que el primer __proto__
es un objeto, y es así.
En realidad cada __proto__
hará referencia al prototipo que se le fue asignado a cierta instancia cuando fue creada. Es por ello que vemos cómo Pikachu, su última instancia fue la que hicimos al constructor PokemonConNew()
Pero oh! Esa no era la única instancia, si vemos, tenemos otro __proto__
que también es otro objeto, pero si buscamos de dónde vino esa instancia (Buscando la propiedad constructor
), nos daremos cuenta que viene de una función constructora llamada Object 😮❕
Y luego tenemos otro __proto__
pero con tres puntos, y si lo abres, te encontrarás con otro __proto__
, y así hasta llegar a null.
Una revelación? Object.prototype es el prototipo de todos los objetos en JS. Y sí esto lo conocemos como cadena de prototipos, mira esto!
Dentro de JavaScript tenemos prototipos muy complejos como el de los Arrays (De ahí vienen todos los métodos para manipularlos), para funciones, fechas, etc. Pero todos estos derivan del Object.prototype
Así que sí, cuando busquemos cierta propiedad en una instancia, se empezará por la instancia en sí, luego por el primer __proto__
, luego por el __proto__
del mismo, así hasta llegar a Object.prototype, cuando llegue a este y necesite ir a su __proto__
, devolverá undefined
, pues el __proto__
de Object.prototype es null
.
Felicidades, ahora ya sabes de dónde viene el undefined 🤗
😮 Revelación del Año
Tanto así? Probablemente ya sepas de esto pero si no, te va a fascinar.
Quizás ya has escuchado sobre cómo crear un Array vacío, cierto?
const arrayVacio = []
Pero qué pasaría si te digo que esos dos corchetes no son en realidad lo que parece.
Pues esos corchetes en realidad son..
const arrayVacio = new Array()
Azúcar sintáctica. De qué?
Sí! De una nueva instancia de un constructor.
Alguna vez te preguntaste, de dónde vienen todos esos métodos que nos permiten operar y trabajar a gusto con arrays en JS?. Creo que ya puedes inferirlo. Efectivamente! Proviene del prototype de esa función constructora Array(). Veámoslo.
Ahora te traigo además, algunos tips que te ayudará bastante cuando desarrolles en JS y te topes con prototypes.
hasOwnProperty
Te ayuda a ver si cierta instancia tiene como propiedad dentro de su propia declaración y no en el prototype que se le haya pasado.
Pikachu.hasOwnProperty(name) // True
Pikachu.hasOwnProperty(caminar) // False
Object.getPrototypeOf
Como puedes adivinar, este te devolverá el prototype con el que fue instanceado cierto objeto. Por ejemplo
const protoypePikachu = Object.getPrototypeOf(Pikachu)
// Devolverá un objeto con todos los métodos de pokemones
Tan tan! Felicidades acabas de entender lo que es prototype en JS! 🥳🎉
Ahora nos toca ver cómo con la llegada de ES6 podemos usar azúcar sintáctica para poder escribir clases como en otros lenguajes, usando un constructor interno y ..
Jajaja, solo bromeo, en realidad es un tema muy interesante que me gustaría redactar próximamente.
Pero por ahora, déjame quitarme el sombrero pues acabas de dominar el tema 😼🏆
Gracias en serio por leer este artículo, cualquier corrección o aporte será bienvenido.
Top comments (0)