El Módulo 4 terminó con un problema muy concreto: cada tarea de Nómada Tareas tiene que saber describirse, comprobar si está vencida y cambiar de estado, pero escribir esos métodos dentro de cada objeto significa que seiscientas tareas arrastrarían seiscientas copias del mismo código. Hace falta un molde compartido. En casi todos los lenguajes ese molde se llama «clase» y funciona por copia de una plantilla. En JavaScript el mecanismo es distinto y más simple de lo que parece: cada objeto guarda un enlace a otro objeto, y cuando le pides una propiedad que no tiene, la busca ahí. Ese enlace se llama prototipo, y es el corazón del lenguaje. En esta lección vas a entenderlo del todo: cómo se resuelve el acceso a una propiedad paso a paso, qué hace exactamente new —cerrando la deuda que dejamos anotada en 04-02—, cómo se construye una jerarquía de herencia a mano y por qué las clases que verás en la lección siguiente no son más que una fachada elegante sobre todo esto.

Contenido

  1. El problema de repetir métodos objeto a objeto
  2. Cada objeto tiene un enlace [[Prototype]]
  3. La cadena de prototipos: cómo se resuelve el acceso a una propiedad
  4. Leer y escribir el prototipo: getPrototypeOf, setPrototypeOf, __proto__
  5. Object.create: crear un objeto con el prototipo que tú digas
  6. La propiedad prototype de las funciones
  7. Qué hace new exactamente: los cuatro pasos
  8. El constructor Tarea y sus métodos en Tarea.prototype
  9. Por qué esto ahorra memoria
  10. instanceof, constructor e isPrototypeOf
  11. Propiedades propias frente a heredadas
  12. Herencia entre constructores «a mano»
  13. Object.prototype y por qué todo objeto tiene toString
  14. No toques los prototipos nativos
  15. Errores Comunes y Consejos
  16. Ejercicios
  17. Conclusión

  1. El problema de repetir métodos objeto a objeto

Empecemos poniendo el problema delante, con código que ya sabes escribir. Esta es una forma directa de dar comportamiento a una tarea: una función fábrica que devuelve el objeto con sus métodos dentro.

'use strict';

const HOY = '2026-09-20';
const MARCAS = { pendiente: '○', 'en-curso': '▸', hecha: '✓' };

function crearTareaConMetodos(id, titulo, responsable, horasEstimadas, fechaLimite, estado) {
  return {
    id,
    titulo,
    responsable,
    horasEstimadas,
    fechaLimite,
    estado,

    // Los métodos se crean DE NUEVO en cada llamada a la fábrica
    estaVencida(hoy) {
      return this.fechaLimite < hoy && this.estado !== 'hecha';
    },
    descripcion() {
      return `${MARCAS[this.estado]} [${this.id}] ${this.titulo} · ${this.responsable} · ${this.horasEstimadas} h`;
    }
  };
}

const t1 = crearTareaConMetodos(1, 'Rediseñar la sala polivalente', 'Iván', 12, '2026-09-30', 'en-curso');
const t6 = crearTareaConMetodos(6, 'Presupuesto de la carpintería', 'Iván', 5, '2026-09-05', 'pendiente');

console.log(t1.descripcion());        // ▸ [1] Rediseñar la sala polivalente · Iván · 12 h
console.log(t6.estaVencida(HOY));     // true

Funciona perfectamente. Y sin embargo tiene un defecto que se ve en una sola línea:

console.log(t1.descripcion === t6.descripcion);   // false

descripcion no es la misma función en las dos tareas. Cada llamada a la fábrica ha creado un objeto función nuevo, con su propio espacio en memoria, para hacer exactamente lo mismo. Con dos tareas da igual. Con las seiscientas de las que hablaba el cierre del módulo anterior, y con cuatro o cinco métodos por tarea, son miles de funciones idénticas ocupando memoria sin aportar nada.

Lo que queremos es esto:

console.log(t1.descripcion === t6.descripcion);   // true  ← una sola función, compartida

Y para conseguirlo hay que entender el mecanismo de búsqueda de propiedades de JavaScript.

  1. Cada objeto tiene un enlace [[Prototype]]

La regla fundamental, y de ella se deriva todo lo demás:

Todo objeto de JavaScript tiene una referencia interna a otro objeto (o a null), llamada [[Prototype]]. Cuando pides una propiedad que el objeto no tiene, el motor la busca en ese otro objeto. Y si tampoco está allí, en el prototipo de ese, y así sucesivamente.

Los dobles corchetes de [[Prototype]] son la notación que usa la especificación del lenguaje para las ranuras internas: no es una propiedad que puedas escribir con un punto, sino un enlace que el motor mantiene por dentro. Sí existen formas oficiales de consultarlo y modificarlo, que verás en el apartado 4.

Un ejemplo mínimo, sin constructores ni nada complicado:

'use strict';

// Un objeto que hará de "molde": contiene comportamiento compartido
const comportamientoDeTarea = {
  descripcion() {
    return `[${this.id}] ${this.titulo}`;
  }
};

// Un objeto cuyo [[Prototype]] es el anterior
const tarea = Object.create(comportamientoDeTarea);
tarea.id = 1;
tarea.titulo = 'Rediseñar la sala polivalente';

console.log(tarea.descripcion());        // '[1] Rediseñar la sala polivalente'
console.log(Object.hasOwn(tarea, 'descripcion'));   // false  ← no la tiene él

Fíjate en lo que acaba de pasar: tarea no tiene ninguna propiedad descripcion, y aun así tarea.descripcion() funciona. El motor no la ha encontrado en tarea, ha seguido el enlace hasta comportamientoDeTarea, la ha encontrado ahí y la ha ejecutado.

Y hay un detalle crucial, que enlaza directamente con lo que aprendiste en 04-02: aunque la función viva en el prototipo, this sigue siendo el objeto sobre el que se hizo la llamada. Esa es exactamente la regla de las cuatro formas de invocación: this es lo que está a la izquierda del punto, no dónde se escribió la función. Por eso this.id vale 1.

const otra = Object.create(comportamientoDeTarea);
otra.id = 6;
otra.titulo = 'Presupuesto de la carpintería';

console.log(otra.descripcion());                       // '[6] Presupuesto de la carpintería'
console.log(tarea.descripcion === otra.descripcion);   // true  ← ¡la misma función!

Ya tenemos el objetivo del apartado 1 cumplido: una sola función compartida por todas las tareas.

  1. La cadena de prototipos: cómo se resuelve el acceso a una propiedad

Cuando escribes objeto.propiedad, el motor sigue este algoritmo:

  1. ¿Tiene objeto una propiedad propia llamada propiedad? Si sí, devuelve su valor y termina.
  2. Si no, toma el [[Prototype]] de objeto. ¿Es null? Entonces devuelve undefined y termina.
  3. Si no es null, repite el paso 1 sobre ese objeto.

Esa secuencia de objetos enlazados es la cadena de prototipos. Es una lista, no un árbol: cada objeto tiene exactamente un prototipo, y la cadena siempre acaba en null.

flowchart TD
    T["tarea<br/>{ id: 1, titulo: '…' }"] -->|"[[Prototype]]"| P["comportamientoDeTarea<br/>{ descripcion() }"]
    P -->|"[[Prototype]]"| O["Object.prototype<br/>{ toString, hasOwnProperty, valueOf … }"]
    O -->|"[[Prototype]]"| N["null<br/>(fin de la cadena)"]

Sigamos tres accesos distintos sobre ese diagrama:

Acceso Recorrido Resultado
tarea.titulo Encontrado en el primer objeto 'Rediseñar la sala polivalente'
tarea.descripcion No está en tarea → encontrado en comportamientoDeTarea la función
tarea.toString No está en tarea → ni en comportamientoDeTarea → encontrado en Object.prototype la función nativa
tarea.responsable No está en ninguno; la cadena acaba en null undefined

Y ahora la parte que casi nadie explica: leer y escribir no son simétricos. La cadena solo se recorre al leer. Al asignar, JavaScript crea (o modifica) una propiedad propia en el objeto de la izquierda, sin tocar el prototipo.

'use strict';

const molde = { prioridad: 'media' };
const a = Object.create(molde);
const b = Object.create(molde);

console.log(a.prioridad, b.prioridad);          // 'media' 'media'  ← ambas heredan

a.prioridad = 'alta';                            // NO modifica el molde

console.log(a.prioridad);                        // 'alta'   ← propiedad propia nueva
console.log(b.prioridad);                        // 'media'  ← intacta
console.log(molde.prioridad);                    // 'media'  ← intacto
console.log(Object.hasOwn(a, 'prioridad'));      // true
console.log(Object.hasOwn(b, 'prioridad'));      // false

A esto se le llama sombreado (shadowing): la propiedad propia de a tapa a la heredada, igual que en 03-04 una variable local tapaba a una de fuera. Es exactamente el comportamiento que queremos: cada tarea tiene sus datos propios y comparte el comportamiento del prototipo.

Cuidado con la excepción: si la propiedad heredada es un objeto o un array y lo mutas en lugar de reasignarlo, sí estás tocando el compartido. a.etiquetas.push('x') no crea nada nuevo: lee etiquetas de la cadena y modifica ese array, que ven todos. Por eso los prototipos deben contener métodos, no datos mutables.

  1. Leer y escribir el prototipo: getPrototypeOf, setPrototypeOf, __proto__

Hay tres formas de tocar el [[Prototype]] de un objeto ya creado.

'use strict';

const molde = { saludo: 'Hola' };
const obj = Object.create(molde);

// LEER: forma correcta
console.log(Object.getPrototypeOf(obj) === molde);   // true

// ESCRIBIR: forma correcta, pero desaconsejada (ver abajo)
const otroMolde = { saludo: 'Buenas' };
Object.setPrototypeOf(obj, otroMolde);
console.log(obj.saludo);                              // 'Buenas'

// LEGADO: __proto__ hace las dos cosas
console.log(obj.__proto__ === otroMolde);             // true
obj.__proto__ = molde;                                // equivale a setPrototypeOf
console.log(obj.saludo);                              // 'Hola'
Forma Qué hace ¿Usarla?
Object.getPrototypeOf(obj) Devuelve el prototipo , es la forma estándar de leerlo
Object.setPrototypeOf(obj, p) Cambia el prototipo de un objeto existente Solo si no hay alternativa: es muy lento
obj.__proto__ Lee o escribe el prototipo No en código nuevo: es legado, existe solo por compatibilidad
Object.create(p) Crea un objeto nuevo ya con prototipo p , es la forma recomendada

Sobre el rendimiento de setPrototypeOf: los motores de JavaScript optimizan mucho el acceso a propiedades suponiendo que la «forma» de un objeto no cambia. Cambiar el prototipo sobre la marcha invalida todas esas optimizaciones para ese objeto y para el código que lo usa. La regla práctica es: decide el prototipo en el momento de crear el objeto y no lo cambies después.

  1. Object.create: crear un objeto con el prototipo que tú digas

Object.create(proto) crea un objeto vacío cuyo [[Prototype]] es proto. Admite además un segundo argumento con descriptores de propiedad (los verás con detalle en 05-03).

const conPrototipo = Object.create({ a: 1 });
console.log(conPrototipo.a);                               // 1

const sinPrototipo = Object.create(null);
console.log(Object.getPrototypeOf(sinPrototipo));          // null
console.log(sinPrototipo.toString);                        // undefined  ← no hereda nada

Ese Object.create(null) tiene un uso muy concreto que ya te encontraste en 04-01: los diccionarios. Cuando indexabas el backlog porId, las claves venían de datos; si algún dato se llamara 'toString' o 'constructor', un objeto literal normal te daría una sorpresa, porque esas propiedades ya existen heredadas.

const normal = {};
console.log('toString' in normal);          // true   ← heredada, aunque el objeto esté vacío
console.log(normal.constructor);            // [Function: Object]

const diccionario = Object.create(null);
console.log('toString' in diccionario);     // false  ← limpio de verdad

Para un mapa de clave→valor con claves impredecibles, Object.create(null) (o directamente un Map, como viste en 04-05) evita toda esa clase de colisiones.

  1. La propiedad prototype de las funciones

Aquí llega la parte que más confusión genera en todo el tema, y la causa es un desafortunado solapamiento de nombres. Hay dos cosas distintas que se llaman casi igual:

Nombre Quién lo tiene Qué es
[[Prototype]] (se lee con Object.getPrototypeOf) Todos los objetos El enlace que se sigue al buscar una propiedad
.prototype Solo las funciones (las normales, no las flecha) Un objeto normal que se usará como [[Prototype]] de las instancias que cree esa función con new

Dicho en una frase: Tarea.prototype no es el prototipo de Tarea; es el prototipo que tendrán los objetos creados con new Tarea(...).

'use strict';

function Tarea(id, titulo) {
  this.id = id;
  this.titulo = titulo;
}

console.log(typeof Tarea.prototype);                              // 'object'
console.log(Tarea.prototype.constructor === Tarea);               // true

const t = new Tarea(1, 'Rediseñar la sala polivalente');

console.log(Object.getPrototypeOf(t) === Tarea.prototype);        // true  ← ¡esta es la clave!
console.log(Object.getPrototypeOf(Tarea) === Function.prototype); // true  (Tarea es una función)

Cuando declaras una función normal, JavaScript le crea automáticamente un objeto prototype vacío, con una única propiedad no enumerable: constructor, que apunta de vuelta a la función. Las funciones flecha no tienen prototype y por eso no pueden usarse con new; es otra consecuencia de lo que viste en 03-02 y 04-02.

const flecha = () => {};
console.log(flecha.prototype);        // undefined
// new flecha();                      // ✗ TypeError: flecha is not a constructor

  1. Qué hace new exactamente: los cuatro pasos

Ahora sí, cerramos la deuda de 04-02. Cuando escribes new Tarea(1, 'Rediseñar…'), el motor hace cuatro cosas en este orden:

  1. Crea un objeto vacío.
  2. Enlaza su [[Prototype]] a Tarea.prototype.
  3. Ejecuta el cuerpo de Tarea con this apuntando a ese objeto nuevo, pasándole los argumentos.
  4. Devuelve ese objeto, salvo que el cuerpo devuelva explícitamente otro objeto (si devuelve un primitivo o nada, se ignora y se devuelve el objeto nuevo).
flowchart TD
    A["new Tarea(1, 'Rediseñar…')"] --> B["1 · const obj = {}"]
    B --> C["2 · Object.setPrototypeOf(obj, Tarea.prototype)"]
    C --> D["3 · Tarea.call(obj, 1, 'Rediseñar…')<br/>el cuerpo asigna this.id, this.titulo…"]
    D --> E{"¿El cuerpo devolvió<br/>un objeto?"}
    E -->|"Sí"| F["Se devuelve ESE objeto"]
    E -->|"No"| G["Se devuelve obj"]

Puedes comprobarlo escribiendo tu propio new con herramientas que ya conoces: Object.create del apartado 5 y el apply de 04-02.

'use strict';

/** Reimplementación didáctica del operador new. */
function miNew(Constructor, ...argumentos) {
  const obj = Object.create(Constructor.prototype);        // pasos 1 y 2
  const resultado = Constructor.apply(obj, argumentos);    // paso 3
  return typeof resultado === 'object' && resultado !== null ? resultado : obj;   // paso 4
}

function Tarea(id, titulo) {
  this.id = id;
  this.titulo = titulo;
  this.estado = 'pendiente';        // R5: toda tarea nace pendiente
}

Tarea.prototype.descripcion = function () {
  return `[${this.id}] ${this.titulo} (${this.estado})`;
};

const conNew = new Tarea(7, 'Revisar la guillotina');
const conMiNew = miNew(Tarea, 7, 'Revisar la guillotina');

console.log(conNew.descripcion());                     // [7] Revisar la guillotina (pendiente)
console.log(conMiNew.descripcion());                   // [7] Revisar la guillotina (pendiente)
console.log(conMiNew instanceof Tarea);                // true

Que las dos versiones den lo mismo demuestra que new no es magia: es azúcar sobre Object.create + una llamada con this fijado.

Por convención, las funciones pensadas para usarse con new se escriben con inicial mayúscula (Tarea, Tablero, ErrorDeValidacion). No es una regla del lenguaje, es una señal para quien lee el código. Y olvidar el new en modo estricto da un error inmediato, lo cual es una suerte:

'use strict';
// const rota = Tarea(8, 'Sin new');
// ✗ TypeError: Cannot set properties of undefined (setting 'id')
//   porque this es undefined en una llamada simple

  1. El constructor Tarea y sus métodos en Tarea.prototype

Con todas las piezas, este es el modelo de Nómada Tareas escrito con constructor y prototipo. Es la versión «clásica» de lo que en la lección siguiente escribirás con class.

'use strict';

const HOY = '2026-09-20';
const PESOS = { alta: 3, media: 2, baja: 1 };
const MARCAS = { pendiente: '○', 'en-curso': '▸', hecha: '✓' };

/**
 * Constructor de tareas de Nómada Tareas.
 * @param {Object} datos  campos del modelo canónico
 */
function Tarea(datos) {
  this.id = datos.id;
  this.titulo = datos.titulo;
  this.responsable = datos.responsable ?? null;      // R8: nunca cadena vacía
  this.prioridad = datos.prioridad ?? 'media';
  this.estado = datos.estado ?? 'pendiente';         // R5
  this.etiquetas = datos.etiquetas ?? [];
  this.horasEstimadas = datos.horasEstimadas;
  this.fechaLimite = datos.fechaLimite;
  this.revisor = datos.revisor ?? null;
}

// ── Comportamiento compartido: vive UNA sola vez, en el prototipo ──

Tarea.prototype.estaVencida = function (hoy) {
  return this.fechaLimite < hoy && this.estado !== 'hecha';        // R10
};

Tarea.prototype.esfuerzo = function () {
  return (PESOS[this.prioridad] ?? 0) * this.horasEstimadas;
};

Tarea.prototype.descripcion = function () {
  const marca = MARCAS[this.estado] ?? '?';
  return `${marca} [${this.id}] ${this.titulo} · ${this.responsable ?? 'sin asignar'} · ${this.horasEstimadas} h`;
};

const tarea1 = new Tarea({
  id: 1, titulo: 'Rediseñar la sala polivalente', responsable: 'Iván',
  prioridad: 'alta', estado: 'en-curso', etiquetas: ['espacio', 'diseño'],
  horasEstimadas: 12, fechaLimite: '2026-09-30', revisor: 'Marta'
});

const tarea6 = new Tarea({
  id: 6, titulo: 'Presupuesto de la carpintería', responsable: 'Iván',
  prioridad: 'alta', estado: 'pendiente', etiquetas: ['carpintería', 'compras'],
  horasEstimadas: 5, fechaLimite: '2026-09-05', revisor: 'Marta'
});

console.log(tarea1.descripcion());          // ▸ [1] Rediseñar la sala polivalente · Iván · 12 h
console.log(tarea6.estaVencida(HOY));       // true   ← la carpintería, la vencida de siempre
console.log(tarea1.esfuerzo());             // 36     (3 × 12)
console.log(tarea1.descripcion === tarea6.descripcion);   // true  ← objetivo cumplido

Con el backlog completo, los números canónicos siguen saliendo:

const datosBacklog = [
  { id: 1, titulo: 'Rediseñar la sala polivalente',          responsable: 'Iván',  prioridad: 'alta',  estado: 'en-curso',  etiquetas: ['espacio', 'diseño'],               horasEstimadas: 12, fechaLimite: '2026-09-30', revisor: 'Marta' },
  { id: 2, titulo: 'Cartelería del taller de serigrafía',    responsable: 'Marta', prioridad: 'media', estado: 'pendiente', etiquetas: ['serigrafía', 'comunicación'],      horasEstimadas: 6,  fechaLimite: '2026-10-15', revisor: null },
  { id: 3, titulo: 'Actualizar la web de reservas',          responsable: 'Lucía', prioridad: 'alta',  estado: 'pendiente', etiquetas: ['web', 'reservas'],                 horasEstimadas: 14, fechaLimite: '2026-10-02', revisor: 'Iván' },
  { id: 4, titulo: 'Inventario de tintas de serigrafía',     responsable: 'Marta', prioridad: 'baja',  estado: 'hecha',     etiquetas: ['serigrafía', 'almacén'],           horasEstimadas: 3,  fechaLimite: '2026-09-12', revisor: null },
  { id: 5, titulo: 'Guía de encuadernación para residentes', responsable: 'Iván',  prioridad: 'media', estado: 'en-curso',  etiquetas: ['encuadernación', 'documentación'], horasEstimadas: 8,  fechaLimite: '2026-11-05', revisor: 'Lucía' },
  { id: 6, titulo: 'Presupuesto de la carpintería',          responsable: 'Iván',  prioridad: 'alta',  estado: 'pendiente', etiquetas: ['carpintería', 'compras'],          horasEstimadas: 5,  fechaLimite: '2026-09-05', revisor: 'Marta' }
];

const backlog = datosBacklog.map((d) => new Tarea(d));

const abiertas = backlog.filter((t) => t.estado !== 'hecha');
console.log(backlog.length);                                              // 6
console.log(backlog.reduce((s, t) => s + t.horasEstimadas, 0));           // 48
console.log(abiertas.reduce((s, t) => s + t.horasEstimadas, 0));          // 45
console.log(abiertas.filter((t) => t.estaVencida(HOY)).length);           // 1
console.log(backlog.reduce((s, t) => s + t.esfuerzo(), 0));               // 124

Los mismos 48, 45, 1 y 124 de siempre —pero ahora cada elemento del array es una Tarea, con comportamiento propio, y map, filter y reduce de 04-05 siguen funcionando exactamente igual.

  1. Por qué esto ahorra memoria

La diferencia entre poner los métodos dentro del constructor o en el prototipo es fácil de ver con un diagrama.

flowchart TD
    subgraph Mal["Métodos dentro del constructor"]
        A1["tarea1<br/>datos + estaVencida + esfuerzo + descripcion"]
        A2["tarea2<br/>datos + estaVencida + esfuerzo + descripcion"]
        A3["tarea3<br/>datos + estaVencida + esfuerzo + descripcion"]
    end
    subgraph Bien["Métodos en el prototipo"]
        B1["tarea1<br/>solo datos"] --> BP["Tarea.prototype<br/>estaVencida · esfuerzo · descripcion"]
        B2["tarea2<br/>solo datos"] --> BP
        B3["tarea3<br/>solo datos"] --> BP
    end

Con N tareas y M métodos:

Estrategia Objetos función creados Con N = 600, M = 3
Métodos como propiedades propias (this.descripcion = function…) N × M 1 800 funciones
Métodos en el prototipo M 3 funciones

Y hay un segundo beneficio, menos citado pero igual de importante: si corriges un método, la corrección alcanza a todas las instancias ya creadas, porque todas leen del mismo objeto.

Tarea.prototype.descripcion = function () {
  return `${MARCAS[this.estado]} #${this.id} ${this.titulo}`;   // nuevo formato
};

console.log(tarea1.descripcion());   // ▸ #1 Rediseñar la sala polivalente
console.log(tarea6.descripcion());   // ○ #6 Presupuesto de la carpintería

Ninguna de las dos tareas se creó de nuevo: simplemente resuelven descripcion en la cadena y encuentran la versión actual. (Este poder también es peligroso; el apartado 14 explica dónde está el límite.)

El coste, para ser honestos: leer una propiedad heredada obliga a recorrer un eslabón más de la cadena. Es una diferencia irrelevante en la práctica —los motores cachean el resultado— pero explica por qué cadenas de diez niveles no son buena idea.

  1. instanceof, constructor e isPrototypeOf

Ya usaste instanceof en 04-08 sin explicación. Ahora puedes entender qué hace exactamente: obj instanceof F recorre la cadena de prototipos de obj buscando el objeto F.prototype.

console.log(tarea1 instanceof Tarea);      // true   ← Tarea.prototype está en su cadena
console.log(tarea1 instanceof Object);     // true   ← Object.prototype también
console.log([] instanceof Array);          // true
console.log([] instanceof Object);         // true
console.log(tarea1 instanceof Array);      // false

Los otros dos mecanismos relacionados:

// constructor: la propiedad que el prototipo lleva de serie
console.log(tarea1.constructor === Tarea);        // true
console.log(tarea1.constructor.name);             // 'Tarea'

// isPrototypeOf: pregunta al revés, desde el prototipo
console.log(Tarea.prototype.isPrototypeOf(tarea1));   // true
console.log(Object.prototype.isPrototypeOf(tarea1));  // true
Herramienta Pregunta que responde Advertencia
obj instanceof F ¿Está F.prototype en la cadena de obj? Es lo que se usa en el 99 % de los casos
obj.constructor ¿Qué función figura como constructora? Es una propiedad normal y sobrescribible: no es fiable como comprobación de seguridad
P.isPrototypeOf(obj) ¿Es P un eslabón de la cadena de obj? Útil cuando tienes el prototipo pero no el constructor (objetos de Object.create)

El aviso sobre constructor no es teórico. Si reemplazas el objeto prototype entero en lugar de añadirle propiedades, pierdes esa referencia:

function Nota(texto) { this.texto = texto; }

Nota.prototype = {                    // ✗ se ha reemplazado el objeto entero
  leer() { return this.texto; }
};

const n = new Nota('hola');
console.log(n.leer());                // 'hola'  (funciona)
console.log(n.constructor === Nota);  // false   ← se perdió
console.log(n.constructor === Object);// true    ← ahora apunta al literal

// Solución si haces esto: restaurarlo a mano
Nota.prototype.constructor = Nota;

La forma segura es añadir métodos uno a uno (Nota.prototype.leer = …), como en el apartado 8.

  1. Propiedades propias frente a heredadas

Esta distinción, que en 04-01 introdujiste con Object.hasOwn, ahora cobra todo el sentido.

console.log(Object.hasOwn(tarea1, 'titulo'));        // true   ← dato propio
console.log(Object.hasOwn(tarea1, 'descripcion'));   // false  ← heredado del prototipo
console.log('descripcion' in tarea1);                // true   ← in SÍ mira la cadena

La diferencia entre in y Object.hasOwn es precisamente si se recorre o no la cadena de prototipos:

Expresión ¿Mira el objeto? ¿Mira la cadena?
Object.hasOwn(obj, 'x') No
obj.hasOwnProperty('x') No (pero el método sí se hereda; Object.hasOwn es la versión moderna y segura)
'x' in obj

Y ahora se explica el comportamiento de for...in que en 04-01 se te presentó como una advertencia: for...in recorre las propiedades enumerables propias y heredadas.

'use strict';

function Punto(x) { this.x = x; }
Punto.prototype.describir = function () { return `x=${this.x}`; };

const p = new Punto(5);

for (const clave in p) console.log(clave);
// x
// describir        ← ¡el método heredado también!

console.log(Object.keys(p));            // [ 'x' ]  ← solo las propias

¿Por qué entonces for...in sobre las tareas del apartado 8 no imprimía descripcion? Porque sí lo haría. Es exactamente la razón por la que en 04-01 la recomendación fue usar Object.keys/values/entries, que solo consideran las propiedades propias. Si por algún motivo necesitas for...in, la protección clásica es filtrar:

for (const clave in p) {
  if (!Object.hasOwn(p, clave)) continue;
  console.log(clave);        // solo 'x'
}

Un apunte para completar el cuadro: las propiedades que trae Object.prototype (toString, valueOf, hasOwnProperty…) no aparecen en for...in porque están marcadas como no enumerables. Los métodos que tú añades a un prototipo con una asignación normal sí son enumerables, y por eso sí aparecen. En 05-03 verás cómo controlar eso con Object.defineProperty, y en la lección siguiente descubrirás que los métodos de class son no enumerables por defecto —una de las muchas mejoras discretas que aportan.

  1. Herencia entre constructores «a mano»

Taller Nómada tiene tareas que se repiten cada semana: recoger el material de serigrafía, revisar las reservas. Una tarea recurrente es una tarea con todo lo de una tarea normal, más una periodicidad. Es el caso de libro de la herencia: queremos que TareaRecurrente tenga todo el comportamiento de Tarea y añada lo suyo.

Con constructores, la herencia se monta en dos pasos que hay que hacer explícitamente.

'use strict';

// ── Paso 1: heredar los DATOS ─────────────────────────────────────
function TareaRecurrente(datos) {
  Tarea.call(this, datos);              // ejecuta el constructor padre sobre ESTE objeto
  this.periodicidad = datos.periodicidad ?? 'semanal';
}

// ── Paso 2: heredar el COMPORTAMIENTO ─────────────────────────────
TareaRecurrente.prototype = Object.create(Tarea.prototype);
TareaRecurrente.prototype.constructor = TareaRecurrente;   // restaurar la referencia

// ── Métodos propios del hijo ──────────────────────────────────────
TareaRecurrente.prototype.proximaFecha = function () {
  const dias = { semanal: 7, quincenal: 14, mensual: 30 };
  const base = new Date(this.fechaLimite);
  base.setDate(base.getDate() + (dias[this.periodicidad] ?? 7));
  return base.toISOString().slice(0, 10);
};

// ── Sobrescritura: el hijo redefine un método del padre ───────────
TareaRecurrente.prototype.descripcion = function () {
  const base = Tarea.prototype.descripcion.call(this);     // llama a la versión del padre
  return `${base} · se repite ${this.periodicidad}`;
};

const limpieza = new TareaRecurrente({
  id: 8, titulo: 'Recoger el material de serigrafía', responsable: 'Marta',
  prioridad: 'baja', horasEstimadas: 1, fechaLimite: '2026-09-25',
  periodicidad: 'semanal'
});

console.log(limpieza.descripcion());
// ○ [8] Recoger el material de serigrafía · Marta · 1 h · se repite semanal
console.log(limpieza.proximaFecha());              // '2026-10-02'
console.log(limpieza.estaVencida(HOY));            // false   ← método heredado del abuelo
console.log(limpieza instanceof TareaRecurrente);  // true
console.log(limpieza instanceof Tarea);            // true

Los dos pasos merecen que los mires por separado, porque son la fuente de casi todos los errores de este tema:

  1. Tarea.call(this, datos) es el call de 04-02 puesto a trabajar. Ejecuta el cuerpo del constructor padre con this apuntando al objeto que se está construyendo, de modo que todas las asignaciones (this.id = …, this.titulo = …) caen en él. Sin esta línea, la tarea recurrente tendría métodos pero ningún dato.
  2. Object.create(Tarea.prototype) crea un objeto vacío enlazado al prototipo del padre, y lo pone como prototype del hijo. Un error frecuentísimo es escribir TareaRecurrente.prototype = Tarea.prototype (sin Object.create): entonces los dos comparten el mismo objeto, y cualquier método que añadas al hijo aparece también en el padre.

Así queda la cadena completa:

flowchart TD
    L["limpieza<br/>{ id: 8, titulo, periodicidad… }"] -->|"[[Prototype]]"| TR["TareaRecurrente.prototype<br/>{ proximaFecha, descripcion, constructor }"]
    TR -->|"[[Prototype]]"| TP["Tarea.prototype<br/>{ estaVencida, esfuerzo, descripcion }"]
    TP -->|"[[Prototype]]"| OP["Object.prototype<br/>{ toString, hasOwnProperty… }"]
    OP -->|"[[Prototype]]"| N["null"]

Y con ese diagrama se explica la sobrescritura: cuando pides limpieza.descripcion, el motor encuentra la versión de TareaRecurrente.prototype antes de llegar a la de Tarea.prototype, así que usa la del hijo. La del padre sigue ahí, tapada, y por eso el hijo puede invocarla explícitamente con Tarea.prototype.descripcion.call(this).

Retén esta ceremonia de cinco líneas —call, Object.create, restaurar constructor, .call para el método del padre—, porque en la lección siguiente se reduce a dos palabras: extends y super.

  1. Object.prototype y por qué todo objeto tiene toString

Al final de casi toda cadena de prototipos está Object.prototype. De ahí vienen métodos que has usado sin preguntarte de dónde salían:

const tarea = { id: 1, titulo: 'Rediseñar la sala polivalente' };

console.log(tarea.toString());                  // '[object Object]'
console.log(tarea.hasOwnProperty('id'));        // true
console.log(tarea.valueOf() === tarea);         // true
console.log(Object.getPrototypeOf(tarea) === Object.prototype);   // true

Ese '[object Object]' que aparece cuando concatenas un objeto con un string —y que en 01-07 te avisamos de que era un mal síntoma— es literalmente el toString heredado de Object.prototype. Puedes darle una implementación mejor a tus tareas:

Tarea.prototype.toString = function () {
  return `Tarea#${this.id} «${this.titulo}»`;
};

console.log(`Registrada: ${tarea1}`);   // 'Registrada: Tarea#1 «Rediseñar la sala polivalente»'
console.log(tarea1 + '');               // 'Tarea#1 «Rediseñar la sala polivalente»'

La conversión implícita a string de 01-07 consulta ese método, así que ahora tus objetos se imprimen de forma útil. Nota que esto es distinto de toJSON (04-08), que solo interviene en JSON.stringify.

Cada tipo integrado tiene su propio prototipo intermedio, todos ellos enlazados a Object.prototype:

Valor Su cadena
[1, 2, 3] Array.prototypeObject.prototypenull
'hola' (al acceder a un método) String.prototypeObject.prototypenull
function f(){} Function.prototypeObject.prototypenull
new Map() Map.prototypeObject.prototypenull
Object.create(null) null (sin cadena)

Eso responde a una pregunta que quizá te hiciste en el Módulo 4: por qué un array tiene map, filter y reduce. No los tiene el array; los tiene Array.prototype, y todos los arrays los encuentran siguiendo su enlace. Lo mismo con 'texto'.toUpperCase(): los strings son primitivos, pero al llamar a un método el motor los envuelve momentáneamente en un objeto String que sí tiene la cadena.

  1. No toques los prototipos nativos

Como acabas de ver que los métodos se comparten por la cadena, la tentación es inmediata: «si añado un método a Array.prototype, todos los arrays del programa lo tendrán».

// ✗✗✗ NO HAGAS ESTO
Array.prototype.ultimo = function () {
  return this[this.length - 1];
};

console.log([1, 2, 3].ultimo());   // 3   (funciona… hasta que deja de funcionar)

Modificar un prototipo nativo se llama monkey patching, y es una de las pocas cosas que la comunidad considera mala práctica casi sin excepciones. Los motivos:

  • Colisiones. Si una librería añade Array.prototype.ultimo con otro comportamiento, una de las dos gana y la otra falla en silencio.
  • Colisiones con el futuro del lenguaje. Ha pasado de verdad: código que añadía Array.prototype.flatten rompió páginas enteras cuando el estándar incorporó flat. El comité tuvo que cambiar el nombre por compatibilidad.
  • Contamina for...in. Como viste en el apartado 11, un método añadido así es enumerable y aparecerá en cualquier for...in sobre un array de cualquier parte del programa.
  • Sorprende a quien lee el código. Un .ultimo() que no está en la documentación de JavaScript obliga a buscar dónde diablos se define.

Las alternativas son siempre mejores:

// ✓ Una función normal
function ultimo(array) {
  return array[array.length - 1];
}

// ✓ O directamente lo que ya trae el lenguaje (04-03)
console.log([1, 2, 3].at(-1));     // 3

La regla completa: añade métodos a los prototipos que tú creas (Tarea.prototype), nunca a los que no son tuyos (Array.prototype, Object.prototype, String.prototype…).

Errores Comunes y Consejos

  • Confundir prototype con [[Prototype]]. Tarea.prototype es una propiedad de la función y solo interesa a la hora de crear instancias; Object.getPrototypeOf(tarea) es el enlace real del objeto. La relación entre ambos es una sola línea: Object.getPrototypeOf(new Tarea()) === Tarea.prototype.
  • Olvidar new. En modo estricto obtienes un TypeError inmediato porque this es undefined; sin modo estricto, las asignaciones caen en el objeto global y contaminan todo en silencio. Otra razón más para el 'use strict' que arrastras desde 01-04. Las clases de 05-02 hacen imposible este error.
  • Poner datos mutables en el prototipo. Tarea.prototype.etiquetas = [] parece inofensivo, pero todas las tareas compartirían ese mismo array, y un push desde una lo cambiaría para todas. Los datos van en el constructor (propiedades propias); en el prototipo, solo métodos y constantes inmutables.
  • Reemplazar el objeto prototype entero (Tarea.prototype = { … }) rompe la propiedad constructor y, si ya habías creado instancias, estas siguen enlazadas al objeto antiguo. Añade métodos uno a uno, o restaura constructor explícitamente.
  • Olvidar Padre.call(this, …) al heredar: la instancia hija tiene los métodos pero ningún dato, y todo sale undefined. Es el error número uno de la herencia manual.
  • Escribir Hijo.prototype = Padre.prototype en lugar de Object.create(Padre.prototype): comparten objeto, y los métodos del hijo se filtran al padre.
  • Usar Object.setPrototypeOf en caliente. Es correcto pero lento; decide el prototipo al crear el objeto.
  • Fiarse de obj.constructor para decidir lógica. Es una propiedad normal que cualquiera puede pisar. Para comprobar el tipo, instanceof.
  • Consejo de depuración: en la consola del navegador, expandir un objeto muestra sus propiedades propias y, al final, una entrada [[Prototype]] que puedes desplegar para ver la cadena entera. Es la forma más rápida de comprobar si la herencia quedó bien montada.

Ejercicios

Ejercicio 1 — La cadena, sobre el papel. Dado este código, indica sin ejecutarlo qué imprime cada console.log y en qué eslabón de la cadena se resuelve cada propiedad.

const base = { tipo: 'tarea', describir() { return `Soy una ${this.tipo}`; } };
const hija = Object.create(base);
hija.tipo = 'subtarea';
const nieta = Object.create(hija);

console.log(nieta.tipo);                       // (a)
console.log(nieta.describir());                // (b)
console.log(Object.hasOwn(nieta, 'tipo'));     // (c)
nieta.tipo = 'microtarea';
console.log(hija.tipo, base.tipo);             // (d)
console.log(Object.keys(nieta));               // (e)

Ejercicio 2 — Constructor Subtarea que hereda de Tarea. Partiendo del constructor Tarea del apartado 8, escribe Subtarea(datos) que:

  • herede datos y comportamiento de Tarea;
  • añada una propiedad padreId;
  • sobrescriba descripcion() para que el resultado vaya sangrado con dos espacios y termine en ← subtarea de #padreId;
  • añada un método esHoja() que devuelva true si this.subtareas está vacío o no existe.

Compruébalo con la subtarea 13 «Pintar y montar» (5 h, pendiente, de la tarea 1), y verifica los dos instanceof.

Ejercicio 3 — contarMetodosPropios. Escribe una función inventario(obj) que devuelva un objeto { propias, heredadas, cadena } donde propias sea el número de propiedades propias enumerables, heredadas el número de propiedades enumerables que solo aparecen en la cadena, y cadena un array con los nombres de los constructores de cada eslabón (por ejemplo ['TareaRecurrente', 'Tarea', 'Object']). Pruébala con la tarea recurrente del apartado 12.

Soluciones

Ejercicio 1

(a) 'subtarea'      → no está en nieta; se encuentra en hija (propia de hija)
(b) 'Soy una subtarea'
      → describir se resuelve en base (dos eslabones arriba),
        pero this es nieta, y this.tipo se resuelve en hija → 'subtarea'
(c) false           → nieta no tiene tipo propio (todavía)
(d) 'subtarea' 'tarea'
      → la asignación creó una propiedad PROPIA en nieta; la cadena no se toca
(e) [ 'tipo' ]      → tras la asignación, nieta ya tiene una propia

El punto (b) es el más instructivo: la función vive en base, pero this se decide en la llamada (nieta.describir()), así que apunta a nieta; y desde ahí, this.tipo vuelve a recorrer la cadena hasta hija. Método y datos se resuelven en eslabones distintos, y eso es completamente normal.

Ejercicio 2

'use strict';

function Subtarea(datos) {
  Tarea.call(this, datos);                 // 1 · datos del padre
  this.padreId = datos.padreId;
  this.subtareas = datos.subtareas ?? [];
}

Subtarea.prototype = Object.create(Tarea.prototype);   // 2 · comportamiento del padre
Subtarea.prototype.constructor = Subtarea;             // 3 · restaurar constructor

Subtarea.prototype.descripcion = function () {
  const base = Tarea.prototype.descripcion.call(this);
  return `  ${base} ← subtarea de #${this.padreId}`;
};

Subtarea.prototype.esHoja = function () {
  return !Array.isArray(this.subtareas) || this.subtareas.length === 0;
};

const pintar = new Subtarea({
  id: 13, titulo: 'Pintar y montar', responsable: 'Iván',
  prioridad: 'media', horasEstimadas: 5, fechaLimite: '2026-09-28',
  padreId: 1
});

console.log(pintar.descripcion());
//   ○ [13] Pintar y montar · Iván · 5 h ← subtarea de #1
console.log(pintar.esHoja());              // true
console.log(pintar.estaVencida(HOY));      // false   ← heredado de Tarea.prototype
console.log(pintar.esfuerzo());            // 10      (media = 2 × 5 h)
console.log(pintar instanceof Subtarea);   // true
console.log(pintar instanceof Tarea);      // true

Los tres pasos numerados son la receta completa de la herencia manual. Si omites el primero, pintar.titulo sería undefined; si omites el segundo, pintar.estaVencida no existiría; si omites el tercero, todo funciona pero pintar.constructor mentiría diciendo Tarea.

Ejercicio 3

'use strict';

function inventario(obj) {
  const propias = Object.keys(obj);
  const todas = [];
  for (const clave in obj) todas.push(clave);          // propias + heredadas enumerables

  const cadena = [];
  let actual = Object.getPrototypeOf(obj);
  while (actual !== null) {
    cadena.push(actual.constructor?.name ?? '(sin constructor)');
    actual = Object.getPrototypeOf(actual);
  }

  return {
    propias: propias.length,
    heredadas: todas.length - propias.length,
    cadena
  };
}

console.log(inventario(limpieza));
// { propias: 10, heredadas: 5, cadena: [ 'TareaRecurrente', 'Tarea', 'Object' ] }

Tres cosas que este ejercicio deja claras. Primero, Object.keys y for...in difieren exactamente en las propiedades heredadas enumerables: restando uno del otro las cuentas. Segundo, el while que recorre la cadena con Object.getPrototypeOf hasta llegar a null es el patrón estándar para inspeccionar una jerarquía, y la condición de parada (!== null) es el «caso base» de 03-07 aplicado a una iteración. Y tercero, los cinco heredados son los métodos que pusimos con asignación normal en los dos prototipos: los de Object.prototype no aparecen porque son no enumerables.

Conclusión

Has llegado al mecanismo que sostiene todo el sistema de objetos de JavaScript, y resulta ser una sola idea repetida: cada objeto guarda un enlace a otro objeto, y las propiedades que no encuentra en sí mismo las busca ahí. De esa idea salen todas las consecuencias que has visto. La cadena de prototipos se recorre al leer, pero nunca al escribir, y por eso una asignación crea siempre una propiedad propia que sombrea a la heredada. Object.create(proto) es la forma limpia de fabricar un objeto con la cadena que quieras —incluida la cadena vacía de Object.create(null) para diccionarios—, mientras que Object.getPrototypeOf la consulta, Object.setPrototypeOf la cambia a un coste alto y __proto__ es un legado que no debes escribir.

Has separado por fin las dos cosas que se llaman parecido: [[Prototype]], que tienen todos los objetos, y .prototype, que solo tienen las funciones y que sirve para una cosa concreta: ser el prototipo de las instancias que esa función cree con new. Y has desmontado new en sus cuatro pasos —crear el objeto, enlazarlo al prototype, ejecutar el cuerpo con this apuntando a él, devolverlo—, hasta el punto de haberlo reimplementado con Object.create y apply. Aquella tercera fila de la tabla de invocaciones de 04-02 ya no tiene nada de misterioso.

Sobre esa base has construido el modelo real: function Tarea(datos) con las nueve propiedades del modelo canónico como datos propios, y estaVencida, esfuerzo y descripcion viviendo una sola vez en Tarea.prototype. Los números de siempre —48 h, 45 abiertas, la carpintería vencida, esfuerzo 124— salen igual, pero ahora con tres funciones en memoria en lugar de mil ochocientas. Sabes distinguir propiedades propias de heredadas con Object.hasOwn frente a in, entiendes por fin por qué for...in recorre las heredadas y Object.keys no, y puedes comprobar tipos con instanceof, isPrototypeOf y —con reservas— constructor. Has montado una herencia completa a mano con Padre.call(this, …) y Object.create(Padre.prototype), sobrescribiendo un método y llamando a la versión del padre. Y sabes por qué Array.prototype tiene map, por qué todo objeto responde a toString, y por qué nunca debes añadir nada a esos prototipos que no son tuyos.

Toda esa ceremonia funciona, pero es ruidosa: tres líneas de ritual por cada relación de herencia, un constructor que hay que restaurar a mano, métodos que se declaran lejos del constructor y que además quedan enumerables sin querer. Desde 2015 el lenguaje ofrece una sintaxis que hace exactamente esto mismo —sin cambiar el mecanismo ni un ápice— pero de forma legible, segura y con las correcciones ya aplicadas de serie. Es el tema de Clases y Programación Orientada a Objetos, donde reescribirás Tarea, TareaRecurrente y un Tablero completo con class, extends y super, y donde entenderás por fin, del todo, aquel class ErrorDeValidacion extends Error que llevas usando como receta desde el Módulo 2.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados