learning-front

Extra · JavaScript a fondo (opcional)

Prototipos a fondo

El modelo real bajo las clases: cada objeto enlaza a un prototipo del que hereda, la cadena de prototipos resuelve de dónde sale cada método heredado, y class es azúcar sobre todo eso.

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:

javascript
// 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:

javascript
// 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:

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

javascript
// 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—:

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

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

  • this apunta al objeto que llama al método, no al que lo define. Por eso describir heredado funciona para cada héroe: this es 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.
  • instanceof recorre la cadena buscando el prototype de la clase. Por eso un Tanque es instancia de Tanque y de Heroe: ambos están en su cadena.
  • No toques los prototipos nativos. Añadir métodos a Array.prototype o Object.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:

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