Pregunta 1RecurringTask no define complete. ¿En qué nivel se encuentra la definición de weekly.complete()?
Resumen de POO — sintaxis de clases y this de un vistazo
Tablas de referencia rápida sobre las clases de esta categoría: dónde vive cada miembro, el orden de búsqueda, cómo se fija this, dónde agregar funcionalidades y cuándo fallan.
En el código de un gestor de tareas terminan mezclados en un mismo archivo los miembros static y #, las clases hijas creadas con extends y los métodos que se sacan de los objetos. Si no tienes claras sus diferencias, no puedes rastrear de dónde sale un undefined o un TypeError.
En este artículo repasarás la sintaxis de esta categoría con tablas de referencia rápida, centradas en la sintaxis de clases y en this.
Leer los miembros según dónde viven — la sintaxis de clases de un vistazo
Supón que estás leyendo una clase Task que escribió otra persona. Las líneas con static, # y get están una junto a otra dentro de las mismas llaves, y leerlas una por una no te dice por qué task.fromJSON(text) lanza un TypeError ni por qué el estado de la tarea no aparece en JSON.stringify(task).
La forma en que escribes algo en el cuerpo de una clase decide dónde vive: en la propia instancia, en Task.prototype (donde van los métodos que comparten todas las instancias) o en la clase misma. Cuando se lee un nombre, JavaScript busca primero en la propia instancia, luego en Task.prototype y después en Object.prototype.
class Task {
static count = 0; // Existe una sola vez, en Task mismo
priority = "normal"; // Se agrega a cada task en cada new
#done = false; // Se guarda en task, pero no se lee fuera de la clase
constructor(title) {
this.title = title;
Task.count += 1;
}
complete() { this.#done = true; } // Va en Task.prototype y lo comparten todas las instancias
get status() { return this.#done ? "completada" : "sin empezar"; }
static fromJSON(text) { return new Task(JSON.parse(text).title); }
}
// Comprueba qué nombres puedes leer desde una instancia y cuáles no
const task = Task.fromJSON('{"title":"Enviar el presupuesto"}');
task.complete();
console.log(task.status, Task.count); // completada 1
console.log(Object.keys(task).join(", ")); // priority, title
console.log(JSON.stringify(task)); // {"priority":"normal","title":"Enviar el presupuesto"}
console.log(task.hasOwnProperty("complete"), typeof task.fromJSON); // false undefined
- Busca en orden: task → Task.prototype → Object.prototype
title,priority— aparecen como claves y en el JSON
complete(),get status()— se encuentran al buscar
#done— se guarda en task, pero solo se lee comothis.#donedentro de la clase
count,fromJSON()— fuera del rango de búsquedatask.fromJSONesundefined
status vive en Task.prototype, y #done está en task pero no aparece como clave, así que ninguno de los dos sale en Object.keys ni en JSON.stringify. Para incluirlos en el JSON, devuelve los valores que quieras desde toJSON. La tabla de abajo ordena la sintaxis de esta categoría según dónde vive cada miembro.
| Sintaxis | Dónde vive | Leído desde task |
|---|---|---|
| this.title en el constructor | task en sí | En claves y JSON |
| Campo de clase priority | task en sí | En claves y JSON |
| Campo privado #done | task (solo en la clase) | Ni en claves ni en JSON |
| Método complete() | Task.prototype | Se llama; no es clave |
| get / set status | Task.prototype | Se lee sin () |
| static count / fromJSON() | Task en sí | undefined |
Rastrear qué definición se ejecuta — extends y super
Piensa en una tarea que se repite cada semana, creada con class RecurringTask extends Task. Con solo mirar la línea weekly.describe() no sabes si se ejecuta el describe de la clase hija o el del padre, ni por qué puedes llamar a complete si la clase hija no lo define.
Un nombre se busca a lo largo de la cadena de prototipos, empezando por el propio objeto, y se ejecuta la primera definición que se encuentra. La definición con la que una clase hija sobrescribe un método se encuentra antes que la del padre, y super.describe() empieza a buscar desde el padre. Los métodos que escribes por separado para cada tipo con polimorfismo también se encuentran en este mismo orden.
class Task {
constructor(title) { this.title = title; }
describe() { return `Tarea: ${this.title}`; }
complete() { return `Hecho: ${this.title}`; }
}
// Clase hija: llama al constructor del padre con super() y sobrescribe solo describe
class RecurringTask extends Task {
constructor(title, weekday) { super(title); this.weekday = weekday; }
describe() { return `${super.describe()} (cada ${this.weekday})`; }
}
// Llama a los métodos para ver qué definición se ejecuta
const weekly = new RecurringTask("Entregar el informe semanal", "viernes");
console.log(weekly.describe()); // Tarea: Entregar el informe semanal (cada viernes)
console.log(weekly.complete()); // Hecho: Entregar el informe semanal
console.log(`${weekly}`); // [object Object]
console.log(weekly instanceof Task, typeof weekly.archive); // true undefined
Un nombre que no está en ningún nivel, como archive, es undefined, y llamarlo lanza un TypeError. ${weekly} da [object Object] porque se ejecuta el toString por defecto del último nivel.
Distinguir this según cómo se llama la función — this de un vistazo
Ahora supón que guardas el método que completa una tarea en una variable para llamarlo después, o que se lo pasas a setTimeout. Un método que funciona bien cuando lo llamas como task.complete() puede terminar con un this que no es task, según cómo lo pases, y entonces lanza un TypeError o this.title sale como undefined.
En qué momento se fija el valor de this depende de cómo está escrita la función. En un método normal de una clase, lo fija lo que está a la izquierda del punto en la línea que la llama; en un campo con función flecha, se fija cuando new crea la instancia. Lo que devuelve bind es otra función, con this fijado al valor que le pasaste a bind.
class Task {
onComplete = () => `Hecho: ${this.title}`; // this es la task creada con new
constructor(title) { this.title = title; }
complete() { return `Hecho: ${this.title}`; } // this lo fija la línea que llama
}
const task = new Task("Enviar la factura");
// Llámalo con task a la izquierda del punto
console.log(task.complete()); // Hecho: Enviar la factura
// Guárdalo en una variable y luego llámalo
const runMethod = task.complete;
const runArrow = task.onComplete;
const runBound = task.complete.bind(task);
console.log(runArrow()); // Hecho: Enviar la factura
console.log(runBound()); // Hecho: Enviar la factura
// runMethod(); // TypeError: Cannot read properties of undefined (reading 'title')
El runBound del código también sigue funcionando con task después de sacarlo. Con setTimeout(task.complete, 100), el navegador pone window en this, así que envuélvelo en una función flecha. La tabla de abajo muestra cuándo se fija this en cada forma de llamar.
| Cómo se llama | Cuándo se fija this | Al sacarla y pasarla |
|---|---|---|
| Método normal | Izquierda del punto al llamar | Pierde task: TypeError |
| Lo que devuelve bind | Al llamar a bind | Sigue con task |
| Campo con flecha | En new | Sigue con task |
| Envuelta en () => task.complete() | Cuando la flecha llama al método | Sigue con task |
| Método agregado con Object.assign | Izquierda del punto al llamar | Pierde su objeto destino |
El método de la clase hija con el mismo nombre nunca se ejecuta
Aunque escribas class RecurringTask extends Task { onComplete() { … } }, el campo con función flecha del padre Task se coloca en el propio weekly y se encuentra primero, así que weekly.onComplete() ejecuta la función del padre. Si las clases hijas necesitan sobrescribirla, defínela también como método en el padre.
Elegir dónde agregar una funcionalidad — herencia, prototype o composición
Vas a agregar funcionalidades a un gestor de tareas que ya está en uso: fijar una sola tarea, mostrar una etiqueta en todas las tareas existentes o asignar responsables solo a las tareas de equipo. Si escribes la funcionalidad en el lugar equivocado, una tarea que creías que la tenía lanza un TypeError.
Las formas de agregar funcionalidades que viste en esta categoría se diferencian en dónde se escribe la funcionalidad: en la propia instancia, en Task.prototype o en un nivel nuevo enlazado con extends. Una vez que sabes dónde se escribe, también sabes qué instancias pueden encontrar ese nombre en su cadena.
class Task {
constructor(title) { this.title = title; }
}
const first = new Task("Enviar el presupuesto"); // Primero crea 2 tareas
const second = new Task("Redactar el acta");
// Copia en una sola tarea
Object.assign(first, { pin() { return `Fijada: ${this.title}`; } });
// Asigna a Task.prototype
Task.prototype.label = function () { return `Tarea: ${this.title}`; };
// Crea una clase con un nivel extra agregado por un mixin
const Assignable = (Base) => class extends Base {
assign(name) { return `Asignada a ${name}: ${this.title}`; }
};
class TeamTask extends Assignable(Task) {}
console.log(typeof second.pin, second.label()); // undefined Tarea: Redactar el acta
console.log(typeof first.assign); // undefined
console.log(new TeamTask("Entregar el informe semanal").assign("Ana")); // Asignada a Ana: Entregar el informe semanal
label llega a second, que ya existía, porque second busca en Task.prototype a lo largo de su cadena. Asignar al prototype sirve para clases cuyo cuerpo no puedes modificar. Un método escrito en una clase hija, igual que un mixin, solo llega a las instancias creadas a partir de esa clase hija.
Usa typeof para comprobar lo que agregaste a un solo objeto
Aunque Object.assign copió pin solo en first, instanceof Task da true tanto para first como para second. Como la clase no permite distinguirlos, comprueba que el método exista con typeof task.pin === "function" antes de llamarlo.
Encontrar la causa según cuándo falla — los errores de un vistazo
Cuando el código de una clase lanza un error, la línea donde se detuvo y la que tienes que corregir pueden estar lejos. Un error en el constructor de una clase hija se lanza desde una línea de su interior cuando se ejecuta new, y con un nombre # escrito fuera de la clase ni siquiera aparece la salida de los console.log que están más arriba.
Los errores se dividen en tres grupos según cuándo detienen tu código: mientras se carga el código, cuando new ejecuta el constructor o cuando se llama a un método. Con un SyntaxError al cargar, no se ejecuta ni una línea; en los otros dos casos, primero se ejecutan las líneas anteriores al error y luego se detiene.
class Task {
#done = false;
constructor(title) { this.title = title; }
complete() { return `Hecho: ${this.title}`; }
}
class RecurringTask extends Task {
constructor(title, weekday) {
this.weekday = weekday; // Se usó this antes de super()
super(title);
}
}
console.log("Empieza la comprobación"); // Se muestra con ② y ③, pero no si está la línea ①
// ① Al cargar: console.log(new Task("Enviar la factura").#done);
// SyntaxError: Private field '#done' must be declared in an enclosing class
// ② En new: new RecurringTask("Entregar el informe semanal", "viernes");
// ReferenceError: Must call super constructor in derived class before accessing 'this' …
// ③ Al llamar: const run = new Task("Enviar la factura").complete; run();
// TypeError: Cannot read properties of undefined (reading 'title')
Con ② y ③ se muestra todo lo anterior antes de que se detenga, así que puedes buscar la causa justo después de la última salida. La tabla de abajo reúne los errores que aparecieron en esta categoría, ordenados desde el que se detiene antes.
| Mensaje de error | Qué lo causa | Cómo corregirlo |
|---|---|---|
| SyntaxError: Private field | task.#done fuera de la clase | Léelo con un getter |
| SyntaxError: … export named | Export por defecto leído como { Cart } | Quita las llaves |
| ReferenceError: Must call super | this antes de super() | Llama antes a super() |
| TypeError: … without 'new' | Clase llamada sin new | Agrega new |
| TypeError: Cannot read properties | Método sacado llamado como run() | Usa bind o una flecha |
| RangeError: Maximum call stack | Setter asigna a su nombre / sin super. | Otro nombre / agrega super. |
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Si llamas a un campo con función flecha como const run = task.onComplete; run();, ¿qué es this?
Pregunta 3Si un new que va después de un console.log lanza un ReferenceError, ¿qué pasa con esa salida?