Usaste class en el nivel 2 sin preguntarte qué hace por debajo. Resulta que JavaScript no tenía
clases de verdad: tenía (y tiene) prototipos, y class es una capa de azúcar encima. Este capítulo
abre esa capa: cada objeto enlaza a un prototipo del que hereda; cuando pides algo que el objeto no
tiene, JavaScript sube por una cadena de prototipos hasta encontrarlo. Entender ese motor explica
de dónde salen métodos que nunca escribiste, por qué instanceof acierta y qué cuesta tocar lo que no
debes.
Seguimos con el Team Builder. El hilo es la plantilla: héroes que saben describirse, y la pregunta de si cada uno lleva su copia de los métodos o los comparten.
¿De dónde sale .toString()? Del prototipo#
Crea un objeto literal de la nada y llama a un método que no has definido:
// un objeto sin más: no tiene método toString
const heroe = { nombre: "Tracer" };
// "[object Object]" -> ¿de dónde ha salido?
console.log(heroe.toString());No lo escribiste tú, y funciona. La razón: tu objeto tiene un prototipo, otro objeto del que
hereda propiedades y métodos. Cuando pides heroe.toString y el objeto no lo tiene, JavaScript lo
busca en su prototipo. El de un objeto literal es Object.prototype, que sí trae toString:
// Object.getPrototypeOf(obj) devuelve el prototipo de un objeto: su siguiente eslabón.
// el prototipo de nuestro objeto literal
const proto = Object.getPrototypeOf(heroe);
// true: los objetos literales heredan de Object.prototype
console.log(proto === Object.prototype);
// true: el método vive ahí, no en heroe
console.log("toString" in Object.prototype);La cadena de prototipos: subir hasta null#
La búsqueda no se queda en el primer prototipo. Si la propiedad tampoco está ahí, JavaScript sube al
prototipo del prototipo, y así hasta encontrarla o llegar a null. Eso es la cadena de
prototipos:
const heroe = { nombre: "Tracer" };
// Eslabón a eslabón:
// Object.prototype (donde está toString)
const e1 = Object.getPrototypeOf(heroe);
// null -> el final de la cadena
const e2 = Object.getPrototypeOf(e1);
// true
console.log(e1 === Object.prototype);
// null -> aquí termina: no hay más dónde buscar
console.log(e2);Si JavaScript sube hasta null sin encontrar lo que pides, la propiedad vale undefined (y si
intentabas llamarla como método, da TypeError). No hay magia: es una búsqueda que para en null.
Object.create: montar la cadena a mano#
La forma directa de fijar el prototipo de un objeto es Object.create(proto): crea un objeto nuevo
cuyo prototipo es proto. Sirve para compartir métodos entre varios objetos sin copiarlos en cada
uno:
// Un objeto con los métodos comunes. Será el prototipo de nuestros héroes.
const heroeProto = {
describir() {
// this es el objeto que llame al método
return this.nombre + " (" + this.rol + ")";
},
};
// Cada héroe se crea con heroeProto como prototipo: hereda describir, no lo copia.
const tracer = Object.create(heroeProto);
tracer.nombre = "Tracer";
tracer.rol = "Daño";
const genji = Object.create(heroeProto);
genji.nombre = "Genji";
genji.rol = "Daño";
// 'Tracer (Daño)' -> describir no está en tracer; lo hereda
console.log(tracer.describir());
// true: el MISMO método, compartido por el proto
console.log(tracer.describir === genji.describir);Fíjate en lo último: los dos héroes comparten la misma función describir. No hay copias: ambos la
heredan del prototipo común. Esa es la idea que hace eficiente a JavaScript con miles de objetos.
class es azúcar sobre prototipos#
Aquí encaja todo lo del nivel 2. Cuando escribes una class, sus métodos acaban en
Clase.prototype, y las instancias los heredan por la cadena —exactamente lo que acabamos de hacer a
mano con Object.create—:
class Heroe {
constructor(nombre) {
// los DATOS van en la instancia
this.nombre = nombre;
}
saludar() {
// el MÉTODO acaba en Heroe.prototype
return "Soy " + this.nombre;
}
}
const mercy = new Heroe("Mercy");
// 'Soy Mercy'
console.log(mercy.saludar());
// true: hereda del prototipo de la clase
console.log(Object.getPrototypeOf(mercy) === Heroe.prototype);
// ['nombre'] -> saludar NO es propio de la instancia
console.log(Object.keys(mercy));Y extends no es más que enlazar prototipos: el prototipo de una subclase apunta al de la clase
base, así que las instancias heredan métodos de ambas subiendo la cadena:
class Tanque extends Heroe {
// Tanque.prototype hereda de Heroe.prototype
aguantar() {
// método propio de Tanque
return this.nombre + " levanta el escudo";
}
}
const rein = new Tanque("Reinhardt");
// 'Reinhardt levanta el escudo' -> método de Tanque
console.log(rein.aguantar());
// 'Soy Reinhardt' -> heredado de Heroe, dos eslabones arriba
console.log(rein.saludar());Por qué esto importa#
No vas a escribir Object.create a diario: en 2026 modelas con class, que es más legible. Pero
conocer el motor te ahorra sorpresas:
thisapunta al objeto que llama al método, no al que lo define. Por esodescribirheredado funciona para cada héroe:thises quien hace la llamada.- La resolución de métodos es esa búsqueda por la cadena. Si una subclase redefine un método, su versión “tapa” a la de arriba (la encuentra antes). Eso es el polimorfismo.
instanceofrecorre la cadena buscando elprototypede la clase. Por eso unTanquees instancia deTanquey deHeroe: ambos están en su cadena.- No toques los prototipos nativos. Añadir métodos a
Array.prototypeoObject.prototype(Array.prototype.miMetodo = ...) contamina todos los arrays u objetos del programa, choca con futuras versiones del lenguaje y rompe librerías. Es un antipatrón clásico: tu código vive en tus clases, no enganchado a las del lenguaje.
Y un caso que ya tocaste sin ver la maquinaria: en el capítulo de errores del Nivel 3 ya usaste esto;
ahora ves qué hace por debajo. class ErrorDeValidacion extends Error enlaza tu prototipo al de
Error, y eso es lo que hace que instanceof pueda distinguir tu error de cualquier otro en un
catch:
class ErrorDeValidacion extends Error {
constructor(campo) {
// super() llama al constructor de Error
super("Campo inválido: " + campo);
// y añadimos lo nuestro: qué campo falló
this.campo = campo;
}
}
try {
// lanzamos nuestro error tipado
throw new ErrorDeValidacion("email");
} catch (e) {
// true: es exactamente nuestro tipo…
console.log(e instanceof ErrorDeValidacion);
// true: …y también un Error (dos eslabones más arriba)
console.log(e instanceof Error);
// 'Campo inválido: email' -> heredado de Error por la cadena
console.log(e.message);
}Distinguir catch (e) { if (e instanceof ErrorDeValidacion) … } no es un truco de librería: es la cadena
de prototipos resolviendo instanceof. La misma mecánica que hace funcionar a describir heredado hace
funcionar a tus errores propios.
Pruébalo tú#
En la consola: de dónde sale toString, la cadena de un objeto literal hasta null, métodos
compartidos con Object.create y la cadena de una instancia de clase. Cambia algún dato, o añade un
método a heroeProto y mira cómo lo heredan los dos héroes a la vez. Pulsa Ejecutar (o Ctrl+Enter) para ver la consola.
Comprueba lo que sabes#
Pregunta 1 de 5
Tienes un objeto literal que no define `toString`, pero `heroe.toString()` funciona. ¿De dónde sale ese método?
Tu turno#
Modela la plantilla tres veces: con objetos literales, compartiendo métodos por el prototipo y con una jerarquía de clases por rol. El objetivo no es solo que funcione, sino ver quién comparte qué. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo cambia la respuesta a “¿comparten el método describir?” de un nivel al siguiente.
Ejercicio · en esta página
Del objeto literal a la clase, y al revés
Convierte el roster en objetos de héroe que sepan describir()se. Empieza con objetos literales, comparte luego los métodos por el prototipo y termina con una jerarquía de clases por rol. El objetivo no es solo que funcione: es entender qué pasa por debajo (quién comparte qué y por qué).
Paso 1: Que funcione
- Cada héroe se describe con su rol, su winrate y su especialidad.
- Vale resolverlo con objetos literales que lleven sus propios métodos.
- Compruebas si dos héroes comparten o no el método describir.
Paso 2: Que esté pulido
- Los métodos se comparten por el prototipo (Object.create o class).
- Verificas que heroes[0].describir === heroes[1].describir (lo mismo, compartido).
- Usas Object.getPrototypeOf para confirmar de qué prototipo cuelgan.
Paso 3: Que sea excelente
- Modelas una jerarquía con una clase base y subclases por rol (extends).
- Explicas, en comentarios, cómo la cadena resuelve los métodos e instanceof.
- No tocas ningún prototipo nativo: la jerarquía vive en tus propias clases.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Cada héroe es un objeto literal con SU PROPIA copia de los métodos (winrate y
// describir). El resultado es correcto, pero...
// - heroes[0].describir !== heroes[1].describir: son funciones DISTINTAS. Cada
// héroe arrastra su propio juego de métodos.
// - con 4 héroes da igual; con miles, es memoria desperdiciada en copiar la
// misma función una y otra vez.
// Funciona, pero ignora que JavaScript ya sabe compartir métodos: el prototipo.
// ════════════════════════════════════════════════════════════════════════════
const ROSTER = [
{ nombre: "Reinhardt", rol: "Tanque", victorias: 51, partidas: 90 },
{ nombre: "Tracer", rol: "Daño", victorias: 78, partidas: 120 },
{ nombre: "Mercy", rol: "Apoyo", victorias: 130, partidas: 200 },
{ nombre: "Genji", rol: "Daño", victorias: 72, partidas: 150 },
];
const ESPECIALIDAD = {
Tanque: "aguanta la línea",
Daño: "presiona y elimina",
Apoyo: "sostiene al equipo",
};
function crearHeroe(raw) {
return {
nombre: raw.nombre,
rol: raw.rol,
victorias: raw.victorias,
partidas: raw.partidas,
// Estas dos funciones se crean DE NUEVO para cada héroe: copias independientes.
winrate() {
return this.victorias / this.partidas;
},
describir() {
const pct = (this.winrate() * 100).toFixed(1);
return (
this.nombre +
" (" +
this.rol +
") — winrate " +
pct +
"% — " +
ESPECIALIDAD[this.rol]
);
},
};
}
const heroes = ROSTER.map(crearHeroe);
// La prueba: ¿el método describir de dos héroes es el MISMO objeto-función?
const comparten = heroes[0].describir === heroes[1].describir;
// muestra la plantilla en la consola
console.log("--- Plantilla ---");
for (const h of heroes) {
console.log(h.describir());
}
console.log("--- Notas ---");
console.log(
"¿" +
heroes[0].nombre +
" y " +
heroes[1].nombre +
" comparten el método describir? " +
comparten,
);
console.log(
"Cada héroe tiene su propia copia de winrate y describir: nada se comparte.",
); Por qué este nivel
- Cada héroe es un objeto literal con su propia copia de winrate y describir: lo más directo, y funciona.
- Su límite: heroes[0].describir !== heroes[1].describir. Son funciones distintas; con miles de héroes, memoria duplicada por no compartir.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - Los métodos viven UNA sola vez en un objeto prototipo (heroeProto). Cada
// héroe se crea con Object.create(heroeProto), así que su prototipo ES ese
// objeto y HEREDA los métodos por la cadena de prototipos.
// - heroes[0].describir === heroes[1].describir: ahora SÍ es la misma función,
// compartida. Cero copias duplicadas, da igual cuántos héroes haya.
// - Object.getPrototypeOf(heroe) === heroeProto lo confirma: ese es el eslabón
// del que cuelgan los métodos.
//
// Su límite respecto a Excelente: montar la cadena a mano con Object.create va
// bien para un nivel, pero para una jerarquía por roles (un tanque hace algo
// distinto que un apoyo) class + extends es mucho más legible y es justo el azúcar
// que envuelve todo esto.
// ════════════════════════════════════════════════════════════════════════════
const ROSTER = [
{ nombre: "Reinhardt", rol: "Tanque", victorias: 51, partidas: 90 },
{ nombre: "Tracer", rol: "Daño", victorias: 78, partidas: 120 },
{ nombre: "Mercy", rol: "Apoyo", victorias: 130, partidas: 200 },
{ nombre: "Genji", rol: "Daño", victorias: 72, partidas: 150 },
];
const ESPECIALIDAD = {
Tanque: "aguanta la línea",
Daño: "presiona y elimina",
Apoyo: "sostiene al equipo",
};
// EL prototipo: un objeto con los métodos, definidos una sola vez. Todos los
// héroes lo tendrán como prototipo y heredarán de aquí winrate y describir.
const heroeProto = {
winrate() {
// this es el héroe concreto que llama
return this.victorias / this.partidas;
},
describir() {
const pct = (this.winrate() * 100).toFixed(1);
return (
this.nombre +
" (" +
this.rol +
") — winrate " +
pct +
"% — " +
ESPECIALIDAD[this.rol]
);
},
};
function crearHeroe(raw) {
// Object.create fija el prototipo del objeto nuevo: su cadena empieza en heroeProto.
const h = Object.create(heroeProto);
// los DATOS sí son propios de cada héroe…
h.nombre = raw.nombre;
h.rol = raw.rol;
h.victorias = raw.victorias;
h.partidas = raw.partidas;
// …pero los MÉTODOS los hereda de heroeProto
return h;
}
const heroes = ROSTER.map(crearHeroe);
const comparten = heroes[0].describir === heroes[1].describir;
const heredaDelProto = Object.getPrototypeOf(heroes[0]) === heroeProto;
// muestra la plantilla en la consola
console.log("--- Plantilla ---");
for (const h of heroes) {
console.log(h.describir());
}
console.log("--- Notas ---");
console.log(
"¿" +
heroes[0].nombre +
" y " +
heroes[1].nombre +
" comparten el método describir? " +
comparten,
);
console.log("¿El prototipo de un héroe es heroeProto? " + heredaDelProto);
console.log(
"describir vive una sola vez en el prototipo; todos lo heredan por la cadena.",
); Por qué es mejor que el anterior
- Los métodos viven una vez en heroeProto y cada héroe se crea con Object.create(heroeProto): heredan describir por la cadena.
- Ahora heroes[0].describir === heroes[1].describir (true) y Object.getPrototypeOf(heroe) === heroeProto: el método se comparte.
- Su límite: modelar una jerarquía por roles a mano con Object.create es engorroso; class + extends lo hace legible.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - class + extends montan la misma cadena de prototipos, pero legible. Una clase
// base Heroe con lo común (winrate, describir) y una subclase por rol con su
// especialidad propia. extends ENLAZA los prototipos: Tanque.prototype hereda
// de Heroe.prototype, que hereda de Object.prototype, que termina en null.
// - Los métodos comunes siguen compartidos (viven en Heroe.prototype); describir
// llama a especialidad() y la resolución SUBE la cadena hasta la versión de la
// subclase: ahí está el polimorfismo, gratis.
// - instanceof acierta porque recorre esa misma cadena: un Tanque es instancia de
// Tanque y de Heroe.
//
// Regla de oro: NO se tocan los prototipos nativos (nada de Array.prototype.miMetodo
// = ...). Eso contamina a todos los arrays del programa y es un antipatrón clásico.
// Tu jerarquía vive en TUS clases, no enganchada a las del lenguaje.
// ════════════════════════════════════════════════════════════════════════════
const ROSTER = [
{ nombre: "Reinhardt", rol: "Tanque", victorias: 51, partidas: 90 },
{ nombre: "Tracer", rol: "Daño", victorias: 78, partidas: 120 },
{ nombre: "Mercy", rol: "Apoyo", victorias: 130, partidas: 200 },
{ nombre: "Genji", rol: "Daño", victorias: 72, partidas: 150 },
];
// Clase base: lo común a todos los héroes. Sus métodos viven en Heroe.prototype.
class Heroe {
constructor(nombre, rol, victorias, partidas) {
this.nombre = nombre;
this.rol = rol;
this.victorias = victorias;
this.partidas = partidas;
}
get winrate() {
// calculado, siempre coherente
return this.victorias / this.partidas;
}
especialidad() {
// por defecto; cada subclase la afina
return "cumple su rol";
}
describir() {
const pct = (this.winrate * 100).toFixed(1);
// this.especialidad() se resuelve subiendo la cadena: usa la de la subclase.
return (
this.nombre +
" (" +
this.rol +
") — winrate " +
pct +
"% — " +
this.especialidad()
);
}
}
// Subclases por rol. extends enlaza su prototipo con el de Heroe; super(...) llama
// al constructor base fijando el rol. Solo sobreescriben lo que cambia: especialidad.
class Tanque extends Heroe {
constructor(nombre, victorias, partidas) {
super(nombre, "Tanque", victorias, partidas);
}
especialidad() {
return "aguanta la línea";
}
}
class Dano extends Heroe {
constructor(nombre, victorias, partidas) {
super(nombre, "Daño", victorias, partidas);
}
especialidad() {
return "presiona y elimina";
}
}
class Apoyo extends Heroe {
constructor(nombre, victorias, partidas) {
super(nombre, "Apoyo", victorias, partidas);
}
especialidad() {
return "sostiene al equipo";
}
}
// El rol del dato elige la subclase. Cada héroe se instancia con la suya.
const POR_ROL = { Tanque: Tanque, Daño: Dano, Apoyo: Apoyo };
function crearHeroe(raw) {
// la subclase que toca según el rol
const Clase = POR_ROL[raw.rol];
return new Clase(raw.nombre, raw.victorias, raw.partidas);
}
const heroes = ROSTER.map(crearHeroe);
const comparten = heroes[0].describir === heroes[1].describir;
const todosSonHeroe = heroes.every((h) => h instanceof Heroe);
// muestra la plantilla en la consola
console.log("--- Plantilla ---");
for (const h of heroes) {
console.log(h.describir());
}
console.log("--- Notas ---");
console.log(
"¿" +
heroes[0].nombre +
" y " +
heroes[1].nombre +
" comparten el método describir? " +
comparten +
" (vive en Heroe.prototype)",
);
console.log(
"¿Todos son instancia de Heroe? " +
todosSonHeroe +
" (instanceof recorre la cadena de prototipos)",
);
console.log(
"Cada subclase solo cambia especialidad(); el resto lo hereda por la cadena.",
); Por qué es mejor que el anterior
- Clase base Heroe y subclases por rol con extends: la misma cadena de prototipos, pero legible. describir vive en Heroe.prototype (compartido).
- Polimorfismo gratis: describir llama a especialidad() y la resolución sube la cadena hasta la versión de la subclase. instanceof acierta por la cadena.
- Y la regla dura: NO tocar prototipos nativos (Array.prototype…). Tu jerarquía vive en tus clases, no enganchada a las del lenguaje.