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
Nombres que task puede encontrar
Lo que busca task.nombre
  • Busca en orden: task → Task.prototype → Object.prototype
task en sí
  • title, priority — aparecen como claves y en el JSON
Task.prototype (compartido)
  • complete(), get status() — se encuentran al buscar
Campos privados fuera del alcance de task.nombre
  • #done — se guarda en task, pero solo se lee como this.#done dentro de la clase
Task en sí (static)
  • count, fromJSON() — fuera del rango de búsqueda
  • task.fromJSON es undefined
Los nombres con # y los miembros static quedan fuera de lo que busca task. task.fromJSON no se encuentra, así que llámalo con el nombre de la clase.

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.

SintaxisDónde viveLeído desde task
this.title en el constructortask en síEn claves y JSON
Campo de clase prioritytask en síEn claves y JSON
Campo privado #donetask (solo en la clase)Ni en claves ni en JSON
Método complete()Task.prototypeSe llama; no es clave
get / set statusTask.prototypeSe 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
El orden de los niveles que se recorren desde weekly
weekly(la instancia)Tiene titley weekdayRecurringTask.prototypeEncuentra describe(destino deweekly.describe())Task.prototypeEncuentra complete(destino desuper.describe())Object.prototypeEncuentra toString(destino de${weekly})
describe se encuentra en el segundo nivel y complete en el tercero. El describe del padre solo se ejecuta cuando agregas super.

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')
Cómo se fija this en una función que sacas
const runMethod= task.completeMétodo normal:se fija al llamarrunMethod(): sinobjeto antes delpuntothis es undefinedTypeErrorconst runArrow= task.onCompleteCampo con flecha:se fija en newEn runArrow()this siguesiendo taskHecho: Enviarla factura
En runMethod(), this es undefined. Si la escribes como en la fila de abajo, una función que sacas y pasas a otro lado conserva task.

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 llamaCuándo se fija thisAl sacarla y pasarla
Método normalIzquierda del punto al llamarPierde task: TypeError
Lo que devuelve bindAl llamar a bindSigue con task
Campo con flechaEn newSigue con task
Envuelta en () => task.complete()Cuando la flecha llama al métodoSigue con task
Método agregado con Object.assignIzquierda del punto al llamarPierde 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
Dónde la escribes decide hasta dónde llega
first y secondcreadas con TaskObject.assign(first, pieza)Task.prototype.label = functionextendsAssignable(Task)Va a las clavespropias de firstSe agrega una veza Task.prototypeAgrega un nivelsobre TeamTaskSolo first; ensecond, undefinedLas dos tareasexistentes puedenllamarloSolo new TeamTask(no first/second)
Escrita en el propio first, llega solo a esa tarea; en Task.prototype, llega a las dos tareas existentes. Un mixin solo llega a las instancias de la clase nueva.

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')
Tres momentos en que un error detiene tu código
① Mientras secarga el códigoSyntaxErrorNo se ejecutaninguna línea② new ejecutael constructorReferenceErrorLas líneas previasa new se ejecutan③ Se llamaa un métodoTypeErrorLas líneas previasa la llamadase ejecutan
Cuanto más arriba está la fila, antes se detiene; con ①, ni siquiera aparece el mensaje de comprobación. Si no se muestra nada de nada, sospecha de un SyntaxError al cargar.

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 errorQué lo causaCómo corregirlo
SyntaxError: Private fieldtask.#done fuera de la claseLéelo con un getter
SyntaxError: … export namedExport por defecto leído como { Cart }Quita las llaves
ReferenceError: Must call superthis antes de super()Llama antes a super()
TypeError: … without 'new'Clase llamada sin newAgrega new
TypeError: Cannot read propertiesMétodo sacado llamado como run()Usa bind o una flecha
RangeError: Maximum call stackSetter asigna a su nombre / sin super.Otro nombre / agrega super.
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1RecurringTask no define complete. ¿En qué nivel se encuentra la definición de weekly.complete()?

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?