Todo objeto tiene un prototipo
JavaScript es un lenguaje basado en prototipos. Suena exótico, pero la idea es simple: cada objeto tiene un enlace secreto a otro objeto —su prototipo— y, cuando pides una propiedad que el objeto no tiene, JavaScript sigue ese enlace y la busca allí.
rabbit no tiene una propiedad eats propia. JavaScript primero busca en rabbit, no la encuentra, sigue el enlace del prototipo hasta animal, ahí encuentra eats: true y lo devuelve. Con flies pasa lo mismo: recorre la cadena, no encuentra nada y devuelve undefined.
Este recorrido por la cadena es, en el fondo, todo el mecanismo. La herencia, los métodos, class... todo se apoya en esto.
La cadena de prototipos en JavaScript
La cadena de prototipos no se detiene en un solo eslabón. Un prototipo puede tener a su vez su propio prototipo, y así sucesivamente hasta llegar a null:
Ejecútalo y verás rabbit, luego Object.prototype y por último null. Por eso rabbit.toString() funciona aunque nunca hayas definido toString: ese método vive en Object.prototype, que está en la cima de prácticamente toda la cadena de prototipos.
Cuando JavaScript busca una propiedad, recorre la cadena de prototipos de abajo hacia arriba. La asignación, en cambio, siempre escribe sobre el propio objeto: nunca sube por la cadena. Esta asimetría es clave y suele pillar desprevenida a mucha gente.
Funciones constructoras y .prototype
Antes de que existiera class, la forma habitual de crear muchos objetos parecidos era mediante una función constructora invocada con new:
Al llamar new User("Ada") pasan dos cosas:
- Se crea un objeto nuevo cuyo prototipo apunta a
User.prototype. Userse ejecuta conthisenlazado a ese nuevo objeto.
El método greet no se copia en cada instancia. Vive una sola vez en User.prototype, y tanto ada como boris lo encuentran recorriendo su cadena de prototipos. Por eso la última línea imprime true: es, literalmente, la misma función.
prototype vs __proto__ en JavaScript
Estos dos nombres son el típico punto donde todo el mundo se lía. Están relacionados, pero no son lo mismo.
User.prototypees una propiedad de la función constructora. Es el objeto que pasará a ser el prototipo de las instancias creadas connew User(...).ada.__proto__(oObject.getPrototypeOf(ada)) es el enlace que tiene la instancia y que apunta hacia su prototipo.
En código nuevo, usa Object.getPrototypeOf(obj) en lugar de obj.__proto__. __proto__ es un accesor heredado que se mantiene por compatibilidad; la función es la API oficial.
Las clases son azúcar sintáctico sobre esto
El JavaScript moderno te permite escribir class, pero por debajo sigues trabajando con prototipos. Compara las dos versiones una al lado de la otra:
greet termina en User.prototype, igual que si lo hubieras escrito a mano. La palabra clave class básicamente te da una sintaxis más limpia, reglas más estrictas (tienes que usar new) y una forma más clara de hacer extends, pero el modelo en tiempo de ejecución es idéntico.
Entender esto te ayuda a la hora de leer mensajes de error o depurar this. Un error que menciona "User.prototype.greet" no es ningún nombre interno raro: es exactamente donde vive el método.
La herencia es solo una cadena de prototipos más larga
extends enlaza un prototipo con otro. El prototipo del padre pasa a ser el prototipo del prototipo del hijo:
Cuando haces rex.eat, JavaScript recorre la cadena: primero rex, luego Dog.prototype y finalmente Animal.prototype, donde encuentra eat y lo invoca manteniendo this apuntando a rex. Eso es todo lo que hace extends por detrás: arma la cadena de prototipos por ti.
Crear objetos directamente con un prototipo
Ni siquiera necesitas un constructor. Con Object.create(proto) creas un objeto nuevo usando como prototipo el que tú le indiques:
Nada de class, nada de new, sin función constructora. Dos objetos que comparten un método a través de un mismo prototipo. Esta es la forma más cruda de la herencia prototípica en JavaScript: todo lo demás se construye sobre esta base.
hasOwnProperty: propiedades propias vs heredadas
Como la búsqueda recorre la cadena de prototipos, "foo" in obj devuelve true incluso para propiedades heredadas. Cuando necesitas saber si una propiedad le pertenece realmente al objeto, usa Object.hasOwn (o el clásico hasOwnProperty):
name vive en la instancia. greet vive en el prototipo. El operador in encuentra ambos; Object.hasOwn solo detecta el primero. Esta diferencia se vuelve relevante cuando recorres un objeto con for...in o lo serializas: casi siempre te interesan únicamente las propiedades propias.
No modifiques los prototipos nativos (monkey-patching)
Como Array.prototype lo comparten todos los arrays de tu programa, podrías añadirle métodos:
// Please don't.
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3
El problema no es que no funcione — funciona perfectamente. Lo jodido es que cada librería, cada dependencia y cada versión futura de JavaScript comparten ese mismo espacio de nombres contigo. El día que Array.prototype.last se estandarice como método nativo con una semántica ligeramente distinta, tu código (o el de otra persona) se va a romper de formas muy sutiles. La telenovela de Array.prototype.flatten / Array.prototype.flat es el ejemplo clásico que siempre se cuenta para advertirlo.
Mantén los helpers como funciones independientes:
Una superficie menos en la que chocar.
El modelo mental
Si quitas todo el ruido, los prototipos en JavaScript se reducen a tres reglas:
- Todo objeto tiene un enlace a su prototipo (que puede ser
null). - Las lecturas de propiedades suben por esa cadena; las escrituras no.
class,newyextendsson formas de armar esas cadenas sin tener que escribirObject.createa mano.
Ten presentes estas tres ideas y el comportamiento de this, instanceof, la resolución de métodos y la herencia encajan solos.
Lo que viene: el event loop
Con esto cerramos el modelo de objetos. El siguiente capítulo cambia totalmente de tema: cómo ejecuta JavaScript tu código a lo largo del tiempo. El event loop es lo que hace que los temporizadores, las promesas y async/await se comporten como lo hacen, y es la base de todo lo asíncrono.
Preguntas frecuentes
¿Qué es un prototipo en JavaScript?
Cada objeto en JavaScript guarda un enlace interno hacia otro objeto, que es su prototipo. Cuando accedes a una propiedad que no está en el objeto, el motor sube por ese enlace —la llamada cadena de prototipos— buscándola. Gracias a esa cadena, los métodos definidos una sola vez se comparten entre todas las instancias.
¿Cuál es la diferencia entre __proto__ y prototype?
prototype es una propiedad que existe en las funciones constructoras (y en las clases). Es el objeto que pasará a ser el prototipo de las instancias creadas con new. En cambio, __proto__ (o Object.getPrototypeOf(obj)) es el enlace real que tiene una instancia apuntando a su prototipo. Por eso se cumple que instance.__proto__ === Constructor.prototype.
¿Las clases de JavaScript son solo azúcar sintáctico sobre prototipos?
En gran medida, sí. class Foo { bar() {} } acaba colocando bar en Foo.prototype, igual que si hubieras escrito function Foo(){} seguido de Foo.prototype.bar = function(){}. Las clases añaden campos privados, una semántica más estricta y una sintaxis más cómoda para extends y super, pero por dentro sigue habiendo prototipos.
¿Conviene añadir métodos a prototipos nativos como Array.prototype?
Casi nunca. Tocar Array.prototype u Object.prototype afecta a todos los arrays u objetos de tu programa, incluidos los que vienen de librerías externas. Puede chocar con futuras incorporaciones del lenguaje y romper bucles for...in. Lo sensato es dejar tus helpers en funciones o módulos aparte.