Menu

Prototipos en JavaScript: la cadena detrás de cada objeto

Qué son realmente los prototipos en JavaScript, cómo la cadena de prototipos resuelve el acceso a propiedades y cómo la sintaxis class se apoya en ese mismo mecanismo.

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í.

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

Al llamar new User("Ada") pasan dos cosas:

  1. Se crea un objeto nuevo cuyo prototipo apunta a User.prototype.
  2. User se ejecuta con this enlazado 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.prototype es una propiedad de la función constructora. Es el objeto que pasará a ser el prototipo de las instancias creadas con new User(...).
  • ada.__proto__ (o Object.getPrototypeOf(ada)) es el enlace que tiene la instancia y que apunta hacia su prototipo.
index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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):

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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, new y extends son formas de armar esas cadenas sin tener que escribir Object.create a 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.

Aprende a programar con Coddy

COMENZAR