No puedes llegar muy lejos en JavaScript sin tratar con objetos. Son fundamentales para casi todos los aspectos del lenguaje de programación JavaScript. De hecho, aprender a crear objetos es probablemente una de las primeras cosas que estudiaste cuando estabas empezando. Dicho esto, para aprender de forma más efectiva sobre los prototipos en JavaScript, vamos a canalizar nuestro desarrollador junior interior y volver a lo básico.
Los objetos son pares clave/valor. La forma más común de crear un objeto es con llaves {}
y se añaden propiedades y métodos a un objeto usando la notación de puntos.
let animal = {}animal.name = 'Leo'animal.energy = 10animal.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}animal.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}animal.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}
Simple. Ahora lo más probable es que en nuestra aplicación necesitemos crear más de un animal. Naturalmente, el siguiente paso para esto sería encapsular esa lógica dentro de una función que podamos invocar cada vez que necesitemos crear un nuevo animal. Llamaremos a este patrón Functional Instantiation
y llamaremos a la propia función «función constructora», ya que es la responsable de «construir» un nuevo objeto.
Instanciación funcional
function Animal (name, energy) { let animal = {} animal.name = name animal.energy = energy animal.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } animal.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } animal.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)
Así es. Ya llegaremos a eso.
Ahora cada vez que queramos crear un nuevo animal (o más ampliamente hablando una nueva «instancia»), todo lo que tenemos que hacer es invocar nuestra función Animal
, pasándole el nivel del animal name
y energy
. Esto funciona muy bien y es increíblemente sencillo. Sin embargo, ¿puedes detectar algún punto débil con este patrón? El mayor y el que intentaremos resolver tiene que ver con los tres métodos – eat
sleep
, y play
. Cada uno de esos métodos no sólo son dinámicos, sino que también son completamente genéricos. Lo que significa que no hay razón para recrear esos métodos como estamos haciendo actualmente cada vez que creamos un nuevo animal. Sólo estamos desperdiciando memoria y haciendo que cada objeto animal sea más grande de lo necesario. ¿Se te ocurre una solución? ¿Qué pasa si en lugar de volver a crear esos métodos cada vez que creamos un nuevo animal, los movemos a su propio objeto y luego podemos hacer que cada animal haga referencia a ese objeto? Podemos llamar a este patrón Functional Instantiation with Shared Methods
, palabrero pero descriptivo.
Instanciación funcional con métodos compartidos
const animalMethods = { eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount }, sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length }, play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function Animal (name, energy) { let animal = {} animal.name = name animal.energy = energy animal.eat = animalMethods.eat animal.sleep = animalMethods.sleep animal.play = animalMethods.play return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)
Al mover los métodos compartidos a su propio objeto y referenciar ese objeto dentro de nuestra función Animal
, ahora hemos resuelto el problema del desperdicio de memoria y de los objetos animales demasiado grandes.
Object.create
Vamos a mejorar nuestro ejemplo una vez más utilizando Object.create
. En pocas palabras, Object.create te permite crear un objeto que delegará en otro objeto en caso de búsquedas fallidas. Dicho de otro modo, Object.create te permite crear un objeto y siempre que haya una búsqueda fallida de propiedades en ese objeto, puede consultar a otro objeto para ver si ese otro objeto tiene la propiedad. Eso fue un montón de palabras. Veamos algo de código.
const parent = { name: 'Stacey', age: 35, heritage: 'Irish'}const child = Object.create(parent)child.name = 'Ryan'child.age = 7console.log(child.name) // Ryanconsole.log(child.age) // 7console.log(child.heritage) // Irish
Así que en el ejemplo anterior, porque child
fue creado con Object.create(parent)
, siempre que haya una búsqueda de propiedades fallida en child
, JavaScript delegará esa búsqueda en el objeto parent
. Lo que significa que aunque child
no tenga una propiedad heritage
parent
sí la tiene por lo que cuando registre child.heritage
obtendrá la herencia de parent
que era Irish
.
Ahora con Object.create
en nuestro cobertizo de herramientas, ¿cómo podemos utilizarlo para simplificar nuestro código Animal
de antes? Bueno, en lugar de añadir todos los métodos compartidos al animal uno por uno como estamos haciendo ahora, podemos utilizar Object.create para delegar en el objeto animalMethods
. Para sonar realmente inteligentes, vamos a llamar a este Functional Instantiation with Shared Methods and Object.create
🙃
Instanciación funcional con métodos compartidos y Object.create
const animalMethods = { eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount }, sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length }, play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function Animal (name, energy) { let animal = Object.create(animalMethods) animal.name = name animal.energy = energy return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)leo.eat(10)snoop.play(5)
📈 Así que ahora cuando llamemos a leo.eat
, JavaScript buscará el método eat
en el objeto leo
. Esa búsqueda fallará, entonces, debido a Object.create, delegará en el objeto animalMethods
que es donde encontrará eat
.
Hasta aquí, todo bien. Sin embargo, todavía hay algunas mejoras que podemos hacer. Parece un poco «hacky» tener que manejar un objeto separado (animalMethods
) para compartir métodos entre instancias. Eso parece una característica común que uno querría que estuviera implementada en el propio lenguaje. Resulta que es así y es la razón por la que estás aquí – prototype
.
¿Entonces qué es exactamente prototype
en JavaScript? Bueno, en pocas palabras, cada función en JavaScript tiene una propiedad prototype
que hace referencia a un objeto. Anticlimático, ¿verdad? Compruébalo tú mismo.
function doThing () {}console.log(doThing.prototype) // {}
¿Y si en lugar de crear un objeto separado para gestionar nuestros métodos (como estamos haciendo con animalMethods
), simplemente ponemos cada uno de esos métodos en el prototipo de la función Animal
? Entonces lo único que tendríamos que hacer es que en lugar de usar Object.create para delegar en animalMethods
, podríamos usarlo para delegar en Animal.prototype
. Llamaremos a este patrón Prototypal Instantiation
.
Instanciación prototípica
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)leo.eat(10)snoop.play(5)
👏👏👏 Con suerte, acabas de tener un gran momento «aha». De nuevo, prototype
no es más que una propiedad que tiene toda función en JavaScript y, como vimos anteriormente, nos permite compartir métodos entre todas las instancias de una función. Toda nuestra funcionalidad sigue siendo la misma pero ahora en lugar de tener que manejar un objeto separado para todos los métodos, podemos simplemente usar otro objeto que viene incorporado en la propia función Animal
Animal.prototype
.
Vamos. Ir. Profundicemos.
En este punto sabemos tres cosas:
- Cómo crear una función constructora.
- Cómo añadir métodos al prototipo de la función constructora.
- Cómo usar Object.create para delegar las búsquedas fallidas al prototipo de la función.
- Toma un argumento que es un objeto.
- Crea un objeto que delega en el objeto argumento en las búsquedas fallidas.
- Devuelve el nuevo objeto creado.
Esas tres tareas parecen bastante fundacionales para cualquier lenguaje de programación. Es JavaScript realmente tan malo que no hay una forma más fácil, «incorporada» para lograr lo mismo? Como probablemente puedas adivinar a estas alturas, sí la hay, y es utilizando la palabra clave new
.
Lo bueno del enfoque lento y metódico que hemos tomado para llegar hasta aquí es que ahora tendrás una comprensión profunda de lo que hace exactamente la palabra clave new
en JavaScript bajo el capó.
Volviendo a nuestro constructor Animal
, las dos partes más importantes eran la creación del objeto y su devolución. Sin crear el objeto con Object.create
, no podríamos delegar en el prototipo de la función en las búsquedas fallidas. Sin la sentencia return
, no recuperaríamos nunca el objeto creado.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}
Aquí está lo bueno de new
– cuando invocas una función usando la palabra clave new
, esas dos líneas se hacen por ti de forma implícita («under the hood») y el objeto que se crea se llama this
.
Usando los comentarios para mostrar lo que ocurre bajo el capó y asumiendo que el constructor Animal
se llama con la palabra clave new
, se puede reescribir así.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
y sin los comentarios «bajo el capó»
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
De nuevo la razón por la que esto funciona y que el objeto this
se nos crea es porque llamamos a la función constructora con la palabra clave new
. Si dejamos fuera new
cuando invocamos la función, ese objeto this
nunca se crea ni se devuelve implícitamente. Podemos ver el problema con esto en el siguiente ejemplo.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined
El nombre de este patrón es Pseudoclassical Instantiation
.
Si JavaScript no es tu primer lenguaje de programación, puede que te estés poniendo un poco inquieto.
«WTF este tío acaba de recrear una versión más cutre de una Clase» – You
Para aquellos que no estén familiarizados, una Clase te permite crear un plano para un objeto. Entonces, cada vez que creas una instancia de esa Clase, obtienes un objeto con las propiedades y métodos definidos en el plano.
¿Te suena? Eso es básicamente lo que hicimos con nuestra función constructora Animal
anterior. Sin embargo, en lugar de utilizar la palabra clave class
, simplemente utilizamos una función JavaScript normal para recrear la misma funcionalidad. Es cierto que nos ha costado un poco de trabajo extra, así como algunos conocimientos sobre lo que ocurre «bajo el capó» de JavaScript, pero los resultados son los mismos.
Aquí están las buenas noticias. JavaScript no es un lenguaje muerto. Está siendo constantemente mejorado y añadido por el comité TC-39. Lo que significa que aunque la versión inicial de JavaScript no soportaba clases, no hay razón para que no se puedan añadir a la especificación oficial. De hecho, eso es exactamente lo que hizo el comité TC-39. En 2015, EcmaScript (la especificación oficial de JavaScript) 6 se publicó con soporte para las clases y la palabra clave class
. Veamos cómo quedaría nuestra función constructora Animal
anterior con la nueva sintaxis de las clases.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
Muy limpio, ¿verdad?
Entonces, si esta es la nueva forma de crear clases, ¿por qué hemos dedicado tanto tiempo a repasar la forma antigua? La razón es que la nueva forma (con la palabra clave class
) es principalmente sólo «azúcar sintáctico» sobre la forma existente que hemos llamado el patrón pseudoclásico. Para entender completamente la sintaxis de conveniencia de las clases de ES6, primero debes entender el patrón pseudoclásico.
En este punto hemos cubierto los fundamentos del prototipo de JavaScript. El resto de este post lo dedicaremos a entender otros temas «buenos para saber» relacionados con él. En otro post, veremos cómo podemos tomar estos fundamentos y utilizarlos para entender cómo funciona la herencia en JavaScript.
Métodos de array
Hemos hablado en profundidad más arriba sobre cómo si quieres compartir métodos entre instancias de una clase, debes pegar esos métodos en el prototipo de la clase (o función). Podemos ver este mismo patrón demostrado si miramos la clase Array
. Históricamente es probable que hayas creado tus arrays así
const friends =
Resulta que eso es sólo azúcar sobre la creación de una instancia new
de la clase Array
.
const friendsWithSugar = const friendsWithoutSugar = new Array()
Una cosa en la que quizás nunca hayas pensado es cómo cada instancia de un array tiene todos esos métodos incorporados (splice
slice
pop
, etc).
Pues como ahora sabes, es porque esos métodos viven en Array.prototype
y cuando creas una nueva instancia de Array
, utilizas la palabra clave new
que establece esa delegación a Array.prototype
en las búsquedas fallidas.
Podemos ver todos los métodos del array simplemente registrando Array.prototype
.
console.log(Array.prototype)/* concat: ƒn concat() constructor: ƒn Array() copyWithin: ƒn copyWithin() entries: ƒn entries() every: ƒn every() fill: ƒn fill() filter: ƒn filter() find: ƒn find() findIndex: ƒn findIndex() forEach: ƒn forEach() includes: ƒn includes() indexOf: ƒn indexOf() join: ƒn join() keys: ƒn keys() lastIndexOf: ƒn lastIndexOf() length: 0n map: ƒn map() pop: ƒn pop() push: ƒn push() reduce: ƒn reduce() reduceRight: ƒn reduceRight() reverse: ƒn reverse() shift: ƒn shift() slice: ƒn slice() some: ƒn some() sort: ƒn sort() splice: ƒn splice() toLocaleString: ƒn toLocaleString() toString: ƒn toString() unshift: ƒn unshift() values: ƒn values()*/
La misma lógica exacta existe para los Objetos también. Todos los objetos delegarán en Object.prototype
en las búsquedas fallidas, que es la razón por la que todos los objetos tienen métodos como toString
y hasOwnProperty
.
Métodos estáticos
Hasta este punto hemos cubierto el porqué y el cómo de compartir métodos entre instancias de una Clase. Sin embargo, ¿qué pasaría si tuviéramos un método que fuera importante para la Clase, pero que no necesitara ser compartido entre instancias? Por ejemplo, ¿qué pasaría si tuviéramos una función que tomara una matriz de instancias Animal
y determinara cuál es la siguiente que hay que alimentar? Lo llamaremos nextToEat
.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}
No tiene sentido que nextToEat
viva en Animal.prototype
ya que no queremos compartirlo entre todas las instancias. En su lugar, podemos pensar en él como un método de ayuda. Así que si nextToEat
no debería vivir en Animal.prototype
, ¿dónde deberíamos ponerlo? Bueno, la respuesta obvia es que podríamos meter nextToEat
en el mismo ámbito que nuestra clase Animal
y luego referenciarla cuando la necesitemos como haríamos normalmente.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(nextToEat()) // Leo
Ahora esto funciona, pero hay una forma mejor.
Cuando tengas un método que sea específico de una clase en sí, pero que no necesite ser compartido entre instancias de esa clase, puedes añadirlo como una
static
propiedad de la clase.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length } static nextToEat(animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name }}
Ahora, como hemos añadido nextToEat
como una propiedad static
de la clase, vive en la propia clase Animal
(no en su prototipo) y se puede acceder a ella usando Animal.nextToEat
.
const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Dado que hemos seguido un patrón similar a lo largo de este post, vamos a ver cómo lograríamos esto mismo utilizando ES5. En el ejemplo anterior vimos cómo el uso de la palabra clave static
ponía el método directamente en la propia clase. Con ES5, este mismo patrón es tan sencillo como añadir manualmente el método al objeto función.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}Animal.nextToEat = function (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Obtener el prototipo de un objeto
Independientemente del patrón que hayas utilizado para crear un objeto, obtener el prototipo de ese objeto puede llevarse a cabo utilizando el método Object.getPrototypeOf
.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)const prototype = Object.getPrototypeOf(leo)console.log(prototype)// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}prototype === Animal.prototype // true
Hay dos cosas importantes que se desprenden del código anterior.
En primer lugar, notarás que proto
es un objeto con 4 métodos, constructor
eat
sleep
, y play
. Esto tiene sentido. Usamos getPrototypeOf
pasando la instancia, leo
recuperando el prototipo de esa instancia, que es donde viven todos nuestros métodos. Esto nos dice una cosa más sobre prototype
también que no hemos hablado todavía. Por defecto, el objeto prototype
tendrá una propiedad constructor
que apunta a la función original o a la clase de la que se creó la instancia. Lo que esto también significa es que como JavaScript pone una propiedad constructor
en el prototipo por defecto, cualquier instancia podrá acceder a su constructor a través de instance.constructor
.
La segunda conclusión importante de lo anterior es que Object.getPrototypeOf(leo) === Animal.prototype
. Esto también tiene sentido. La función constructora Animal
tiene una propiedad prototype donde podemos compartir métodos entre todas las instancias y getPrototypeOf
nos permite ver el prototipo de la propia instancia.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function
Para enlazar con lo que hablamos antes con Object.create
, la razón por la que esto funciona es porque cualquier instancia de Animal
va a delegar en Animal.prototype
en las búsquedas fallidas. Así que cuando intentes acceder a leo.constructor
leo
no tiene una propiedad constructor
por lo que delegará esa búsqueda en Animal.prototype
que sí tiene una propiedad constructor
. Si este párrafo no tiene sentido, vuelve a leer sobre Object.create
más arriba.
Puede que hayas visto antes el uso de __proto__ para obtener el prototipo de una instancia. Eso es una reliquia del pasado. En su lugar, utiliza Object.getPrototypeOf(instance) como vimos anteriormente.
Determinar si una propiedad vive en el prototipo
Hay ciertos casos en los que necesitas saber si una propiedad vive en la propia instancia o si vive en el prototipo en el que el objeto delega. Podemos ver esto en acción haciendo un bucle sobre nuestro objeto leo
que hemos estado creando. Digamos que el objetivo era el bucle sobre leo
y registrar todas sus claves y valores. Usando un bucle for in
, probablemente se vería así.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)for(let key in leo) { console.log(`Key: ${key}. Value: ${leo}`)}
¿Qué esperarías ver? Lo más probable es que fuera algo así –
Key: name. Value: LeoKey: energy. Value: 7
Sin embargo, lo que veías si ejecutabas el código era esto –
Key: name. Value: LeoKey: energy. Value: 7Key: eat. Value: function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Key: sleep. Value: function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Key: play. Value: function (length) { console.log(`${this.name} is playing.`) this.energy -= length}
¿Por qué? Bueno, un bucle for in
va a recorrer todas las propiedades enumerables tanto del propio objeto como del prototipo en el que delega. Como por defecto cualquier propiedad que añadas al prototipo de la función es enumerable, no sólo vemos name
y energy
, sino que también vemos todos los métodos del prototipo – eat
sleep
, y play
. Para arreglar esto, tenemos que especificar que todos los métodos del prototipo son no enumerables o necesitamos una manera de sólo console.log si la propiedad está en el propio objeto leo
y no en el prototipo que leo
delega en las búsquedas fallidas. Aquí es donde hasOwnProperty
puede ayudarnos.
hasOwnProperty
es una propiedad en cada objeto que devuelve un booleano que indica si el objeto tiene la propiedad especificada como propiedad propia y no en el prototipo en el que el objeto delega. Eso es exactamente lo que necesitamos. Ahora, con este nuevo conocimiento, podemos modificar nuestro código para aprovechar hasOwnProperty
dentro de nuestro bucle for in
.
...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo}`) }}
Y ahora lo que vemos son sólo las propiedades que están en el propio objeto leo
en lugar de en el prototipo leo
que también delega.
Key: name. Value: LeoKey: energy. Value: 7
Si todavía estás un poco confundido sobre hasOwnProperty
, aquí hay algo de código que puede aclararlo.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)leo.hasOwnProperty('name') // trueleo.hasOwnProperty('energy') // trueleo.hasOwnProperty('eat') // falseleo.hasOwnProperty('sleep') // falseleo.hasOwnProperty('play') // false
Comprobar si un objeto es una instancia de una Clase
A veces quieres saber si un objeto es una instancia de una clase específica. Para ello, puedes utilizar el operador instanceof
. El caso de uso es bastante sencillo, pero la sintaxis real es un poco extraña si nunca la has visto antes. Funciona así
object instanceof Class
La sentencia anterior devolverá true si object
es una instancia de Class
y false si no lo es. Volviendo a nuestro ejemplo de Animal
tendríamos algo así.
function Animal (name, energy) { this.name = name this.energy = energy}function User () {}const leo = new Animal('Leo', 7)leo instanceof Animal // trueleo instanceof User // false
La forma en que instanceof
funciona es comprobando la presencia de constructor.prototype
en la cadena de prototipos del objeto. En el ejemplo anterior, leo instanceof Animal
es true
porque Object.getPrototypeOf(leo) === Animal.prototype
. Además, leo instanceof User
es false
porque Object.getPrototypeOf(leo) !== User.prototype
.
Creación de nuevas funciones constructoras agnósticas
¿Puedes detectar el error en el siguiente código?
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)
Incluso los desarrolladores experimentados de JavaScript a veces se tropiezan con el ejemplo anterior. Como estamos utilizando el pseudoclassical pattern
que aprendimos antes, cuando se invoca la función constructora Animal
, tenemos que asegurarnos de invocarla con la palabra clave new
. Si no lo hacemos, entonces la palabra clave this
no se creará y tampoco se devolverá implícitamente.
Como refresco, las líneas comentadas son lo que ocurre entre bastidores cuando se utiliza la palabra clave new
en una función.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}
Esto parece un detalle demasiado importante como para dejarlo en manos de otros desarrolladores. Suponiendo que estamos trabajando en equipo con otros desarrolladores, ¿hay alguna forma de asegurarnos de que nuestro constructor Animal
se invoque siempre con la palabra clave new
? Resulta que sí la hay y es utilizando el operador instanceof
que aprendimos anteriormente.
Si el constructor fue llamado con la palabra clave new
, entonces this
dentro del cuerpo del constructor será un instanceof
la propia función del constructor. Eso fue un montón de palabras grandes. Aquí tienes algo de código.
function Animal (name, energy) { if (this instanceof Animal === false) { console.warn('Forgot to call Animal with the new keyword') } this.name = name this.energy = energy}
Ahora, en lugar de limitarse a registrar una advertencia al consumidor de la función, ¿qué pasa si volvemos a invocar la función pero con la palabra clave new
esta vez?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}
Ahora independientemente de si Animal
se invoca con la palabra clave new
, seguirá funcionando correctamente.
Recreando Object.create
A lo largo de este post, nos hemos apoyado mucho en Object.create
para crear objetos que delegan en el prototipo de la función constructora. En este punto, deberías saber cómo usar Object.create
dentro de tu código, pero algo que quizás no hayas pensado es cómo Object.create
funciona realmente bajo el capó. Para que entiendas realmente cómo funciona Object.create
, vamos a recrearlo nosotros mismos. En primer lugar, ¿qué sabemos acerca de cómo Object.create
funciona?
Empecemos con el nº 1.
Object.create = function (objToDelegateTo) {}
Bastante sencillo.
Ahora el nº 2 – necesitamos crear un objeto que delegue en el objeto argumento en las búsquedas fallidas. Esto es un poco más complicado. Para ello, utilizaremos nuestros conocimientos sobre cómo funcionan la palabra clave new
y los prototipos en JavaScript. En primer lugar, dentro del cuerpo de nuestra implementación Object.create
, crearemos una función vacía. Luego, estableceremos el prototipo de esa función vacía igual al objeto argumento. Luego, para crear un nuevo objeto, invocaremos nuestra función vacía usando la palabra clave new
. Si devolvemos ese objeto recién creado, eso terminará el #3 también.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}
Salvaje. Vamos a recorrerlo.
Cuando creamos una nueva función, Fn
en el código anterior, viene con una propiedad prototype
. Cuando la invocamos con la palabra clave new
, sabemos que lo que obtendremos de vuelta es un objeto que delegará en el prototipo de la función en las búsquedas fallidas. Si sobreescribimos el prototipo de la función, entonces podemos decidir a qué objeto delegar en las búsquedas fallidas. Así que en nuestro ejemplo anterior, sobrescribimos el prototipo de Fn
con el objeto que se pasó cuando se invocó Object.create
al que llamamos objToDelegateTo
.
Nota que sólo estamos soportando un único argumento para Object.create. La implementación oficial también soporta un segundo argumento opcional que permite añadir más propiedades al objeto creado.
Funciones de flecha
Las funciones de flecha no tienen su propia palabra clave this
. Como resultado, las funciones de flecha no pueden ser funciones constructoras y si intentas invocar una función de flecha con la palabra clave new
, arrojará un error.
const Animal = () => {}const leo = new Animal() // Error: Animal is not a constructor
Además, como hemos demostrado anteriormente que el patrón pseudoclásico no se puede utilizar con funciones de flecha, las funciones de flecha tampoco tienen una propiedad prototype
.
const Animal = () => {}console.log(Animal.prototype) // undefined