Prototipos de JavaScript
Prototipos y Herencias de Javascript
Introducción
Prototipos de JavaScript. JavaScript es un lenguaje basado en prototipos , lo que significa que las propiedades de los objetos y los métodos se pueden compartir a través de objetos generalizados que tienen la capacidad de ser clonados y extendidos. Esto se conoce como herencia prototípica y difiere de la herencia de clase. Entre los populares lenguajes de programación orientados a objetos, JavaScript es relativamente único, ya que otros lenguajes prominentes como PHP, Python y Java son lenguajes basados en clases, que en cambio definen las clases como planos para los objetos.
En este tutorial, aprenderemos qué prototipos de objetos son y cómo usar la función constructor para extender los prototipos a nuevos objetos. También aprenderemos sobre la herencia y la cadena de prototipos.
Prototipos de JavaScript
En Descripción de los objetos en JavaScript , revisamos el tipo de datos del objeto, cómo crear un objeto y cómo acceder y modificar las propiedades del objeto. Ahora aprenderemos cómo los prototipos se pueden usar para extender objetos.
Cada objeto en JavaScript tiene una propiedad interna llamada Prototype
. Podemos demostrar esto creando un nuevo objeto vacío.
let x = {};
Esta es la forma en que normalmente creamos un objeto, pero tenga en cuenta que otra forma de lograr esto es con el constructor del objeto: let x = new Object()
.
Los corchetes dobles que encierran Prototype
significan que es una propiedad interna y no se puede acceder directamente en el código.
Para encontrar el Prototype
de este objeto recién creado, usaremos el método getPrototypeOf()
.
Object.getPrototypeOf(x);
La salida consistirá en varias propiedades y métodos integrados.
Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Otra forma de encontrar el Prototype
es a través de la propiedad __proto__
. __proto__
es una propiedad que expone el Prototype
interno Prototype
de un objeto.
Es importante tener en cuenta que .__proto__
es una función heredada y no debe utilizarse en el código de producción, y no está presente en todos los navegadores modernos. Sin embargo, podemos usarlo a lo largo de este artículo para fines demostrativos.
x.__proto__;
La salida será la misma que si hubiera usado getPrototypeOf()
.
Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Es importante que cada objeto en JavaScript tenga un Prototype
ya que crea una forma de vincular dos o más objetos.
Los objetos que crea tienen un Prototype
, al igual que los objetos incorporados, como Date
y Array
. Se puede hacer una referencia a esta propiedad interna de un objeto a otro a través de la propiedad del prototype
, como veremos más adelante en este tutorial.
Herencia del prototipo
Cuando intenta acceder a una propiedad o método de un objeto, JavaScript primero buscará en el objeto, y si no se encuentra, buscará el Prototype
. Si después de consultar tanto el objeto como su Prototype
todavía no se encuentra ninguna coincidencia, JavaScript verificará el prototipo del objeto vinculado y continuará buscando hasta que se llegue al final de la cadena del prototipo.
Al final de la cadena de prototipos está Object.prototype
. Todos los objetos heredan las propiedades y métodos de Object
. Cualquier intento de buscar más allá del final de la cadena da como resultado null
.
En nuestro ejemplo, x
es un objeto vacío que hereda de Object
. x
puede usar cualquier propiedad o método que Object
tenga, como toString()
.
x.toString();
Output
[object Object]
Esta cadena de prototipos es solo un enlace largo. x
-> Object
. Sabemos esto, porque si tratamos de encadenar dos propiedades Prototype
juntas, será null
.
x.__proto__.__proto__;
Output
null
Veamos otro tipo de objeto. Si tiene experiencia trabajando con matrices en JavaScript , sabe que tienen muchos métodos integrados, como pop()
y push()
. La razón por la que tiene acceso a estos métodos cuando crea una nueva matriz es porque cualquier matriz que cree tiene acceso a las propiedades y métodos en Array.prototype
.
Podemos probar esto creando una nueva matriz.
let y = [];
Tenga en cuenta que también podríamos escribirlo como un constructor de arreglos, let y = new Array()
.
Si echamos un vistazo al Prototype
de la nueva matriz y
, veremos que tiene más propiedades y métodos que el objeto x
. Ha heredado todo de Array.prototype
.
y.__proto__;
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]
Notará una propiedad de constructor
en el prototipo que está establecida en Array()
. La propiedad constructor
devuelve la función de constructor de un objeto, que es un mecanismo utilizado para construir objetos a partir de funciones.
Podemos encadenar dos prototipos juntos ahora, ya que nuestra cadena de prototipos es más larga en este caso. Se ve como y
-> Array
-> Object
.
y.__proto__.__proto__;
Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, ...}
Esta cadena ahora se refiere a Object.prototype
. Podemos probar el Prototype
interno Prototype
contra la propiedad del prototype
de la función constructora para ver que se están refiriendo a la misma cosa.
y.__proto__ === Array.prototype; // true
y.__proto__.__proto__ === Object.prototype; // true
También podemos usar el método isPrototypeOf()
para lograr esto.
Array.prototype.isPrototypeOf(y); // true
Object.prototype.isPrototypeOf(Array); // true
Podemos usar el operador instanceof
para probar si la propiedad prototype
de un constructor aparece en cualquier lugar dentro de la cadena de prototipos de un objeto.
y instanceof Array; // true
Para resumir, todos los objetos de JavaScript tienen una propiedad interna Prototype
oculta (que puede estar expuesta a través de __proto__
en algunos navegadores). Los objetos se pueden extender y heredarán las propiedades y métodos en Prototype
de su constructor.
Estos prototipos se pueden encadenar, y cada objeto adicional heredará todo a lo largo de la cadena. La cadena termina con el Object.prototype
.
Funciones Constructor
Las funciones constructor son funciones que se usan para construir nuevos objetos. El new
operador se usa para crear nuevas instancias basadas en una función de constructor. Hemos visto algunos constructores incorporados de JavaScript, como el new Array()
y el new Date()
, pero también podemos crear nuestras propias plantillas personalizadas para construir nuevos objetos.
Como ejemplo, digamos que estamos creando un juego de rol muy simple basado en texto. Un usuario puede seleccionar un personaje y luego elegir la clase de personaje que tendrá, como guerrero, sanador, ladrón, etc.
Dado que cada personaje compartirá muchas características, como tener un nombre, un nivel y puntos de golpe, tiene sentido crear un constructor como plantilla. Sin embargo, dado que cada clase de personaje puede tener habilidades muy diferentes, queremos asegurarnos de que cada personaje solo tenga acceso a sus propias habilidades. Echemos un vistazo a cómo podemos lograr esto con prototipos de herencia y constructores.
Para comenzar, una función de constructor es solo una función regular. Se convierte en un constructor cuando es invocado por una instancia con la new
palabra clave. En JavaScript, capitalizamos la primera letra de una función de constructor por convención.
// Initialize a constructor function for a new Hero
function Hero(name, level) {
this.name = name;
this.level = level;
}
Hemos creado una función de constructor llamada Hero
con dos parámetros: name
y level
. Como cada personaje tendrá un nombre y un nivel, tiene sentido que cada nuevo personaje tenga estas propiedades. this
palabra clave se referirá a la nueva instancia que se crea, por lo que establecer this.name
en el parámetro name
garantiza que el nuevo objeto tendrá una propiedad de name
establecida.
Ahora podemos crear una nueva instancia con new
.
let hero1 = new Hero('Bjorn', 1);
Si hero1
, veremos que se ha creado un nuevo objeto con las nuevas propiedades establecidas como se esperaba.
Output
Hero {name: "Bjorn", level: 1}
Ahora si obtenemos el Prototype
de hero1
, podremos ver el constructor
como Hero()
. (Recuerde, esto tiene la misma entrada que hero1.__proto__
, pero es el método adecuado para usar).
Object.getPrototypeOf(hero1);
Output
constructor: ƒ Hero(name, level)
Puede observar que solo hemos definido propiedades y no métodos en el constructor. Es una práctica común en JavaScript definir los métodos en el prototipo para una mayor eficiencia y legibilidad del código.
Podemos agregar un método a Hero
usando un prototype
. Crearemos un método de greet()
...
// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
return `${this.name} says hello.`;
}
Como greet()
está en el prototype
de Hero
, y hero1
es una instancia de Hero
, el método está disponible para hero1
.
hero1.greet();
Output
"Bjorn says hello."
Si inspecciona el Prototype
de Hero, verá greet()
como una opción disponible ahora.
Esto es bueno, pero ahora queremos crear clases de personajes para que los usen los héroes. No tendría sentido poner todas las habilidades para cada clase en el constructor de Hero
, porque las diferentes clases tendrán diferentes habilidades. Queremos crear nuevas funciones de constructor, pero también queremos que estén conectados al Hero
original.
Podemos usar el método call()
para copiar las propiedades de un constructor a otro constructor. Vamos a crear un Guerrero y un constructor Sanador.
...
// Initialize Warrior constructor
function Warrior(name, level, weapon) {
// Chain constructor with call
Hero.call(this, name, level);
// Add a new property
this.weapon = weapon;
}
// Initialize Healer constructor
function Healer(name, level, spell) {
Hero.call(this, name, level);
this.spell = spell;
}
Ambos nuevos constructores ahora tienen las propiedades de Hero
y algunas únicas. Agregaremos el método attack()
a Warrior
y el método heal()
a Healer
.
...
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`;
}
En este punto, crearemos nuestros personajes con las dos nuevas clases de personajes disponibles.
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');
hero1
ahora se reconoce como un Warrior
con las nuevas propiedades.
Output
Warrior {name: "Bjorn", level: 1, weapon: "axe"}
Podemos usar los nuevos métodos que establecemos en el prototipo de Warrior
.
hero1.attack();
Console
"Bjorn attacks with the axe."
Pero, ¿qué sucede si tratamos de usar métodos más abajo en la cadena de prototipos?
hero1.greet();
Output
Uncaught TypeError: hero1.greet is not a function
Las propiedades y métodos prototipo no se vinculan automáticamente cuando se usa call()
para construir cadenas. Utilizaremos Object.create()
para vincular los prototipos, asegurándonos de ponerlo antes de que se creen y agreguen métodos adicionales al prototipo.
...
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);
// All other prototype methods added below
...
Ahora podemos usar con éxito métodos prototipo de Hero
en una instancia de un Warrior
o Healer
.
hero1.greet();
Output
"Bjorn says hello."
Aquí está el código completo de nuestra página de creación de personajes.
// Initialize constructor functions
function Hero(name, level) {
this.name = name;
this.level = level;
}
function Warrior(name, level, weapon) {
Hero.call(this, name, level);
this.weapon = weapon;
}
function Healer(name, level, spell) {
Hero.call(this, name, level);
this.spell = spell;
}
// Link prototypes and add prototype methods
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);
Hero.prototype.greet = function () {
return `${this.name} says hello.`;
}
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`;
}
// Initialize individual character instances
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');
Con este código creamos nuestra clase Hero
con las propiedades base, creamos dos clases de caracteres llamadas Warrior
y Healer
del constructor original, añadimos métodos a los prototipos y creamos instancias de personajes individuales.
Conclusión
JavaScript es un lenguaje basado en prototipos y funciona de manera diferente al paradigma tradicional basado en clases que utilizan muchos otros lenguajes orientados a objetos.
En este tutorial, aprendimos cómo funcionan los prototipos en JavaScript y cómo vincular las propiedades y los métodos de los objetos a través de la propiedad oculta Prototype
que comparten todos los objetos. También aprendimos cómo crear funciones de constructor personalizadas y cómo funciona la herencia de prototipo para pasar valores de propiedades y métodos.
Fuente. Artículo traducido y con muy ligeras modificaciones de: https://www.digitalocean.com/community/tutorials/understanding-prototypes-and-inheritance-in-javascript