Pregunta 1Para un método summary escrito en una clase, ¿qué devuelve jsCourse.hasOwnProperty("summary")?
Prototipos — lo que hay detrás de una clase
Trata prototype, donde viven los métodos de una clase, y la cadena de prototipos: Object.getPrototypeOf, hasOwnProperty frente a in y cómo agregar métodos después.
Un curso creado a partir de una clase puede llamar a summary(), pero la instancia en sí no tiene ese método. Si no sabes dónde vive, tampoco puedes explicar por qué una clase hija puede llamar a los métodos de su clase padre.
Este artículo trata prototype, donde van los métodos de una clase, y la cadena de prototipos, el orden en que JavaScript busca un nombre.
Llamar a un método que la instancia no tiene — prototype
Si creas 300 cursos, quizá te preguntes si también se crean 300 copias del cuerpo de summary, una por instancia. Object.keys(jsCourse) no incluye summary, así que no te dice dónde se guardan los métodos que llamas.
El prototipo (otro objeto del que un objeto hereda propiedades) se obtiene con Object.getPrototypeOf(), y en el caso de jsCourse es Course.prototype. Cualquier nombre que el objeto no tenga se busca ahí.
La propiedad prototype de la clase es ese mismo objeto, pero jsCourse.prototype es undefined.
class Course {
constructor(title, hours) {
this.title = title; // Nombre del curso
this.hours = hours; // Horas de estudio
}
summary() { return `${this.title} (${this.hours} horas)`; }
}
const jsCourse = new Course("JS básico", 12);
const sqlCourse = new Course("SQL básico", 8);
// summary no está entre las claves propias de la instancia
console.log(Object.keys(jsCourse).join(", ")); // title, hours
// El prototipo de jsCourse es Course.prototype (no jsCourse.prototype)
console.log(Object.getPrototypeOf(jsCourse) === Course.prototype); // true
console.log(jsCourse.prototype); // undefined
// Los dos cursos usan el único summary que hay en Course.prototype
console.log(jsCourse.summary === sqlCourse.summary); // true
console.log(sqlCourse.summary()); // SQL básico (8 horas)
Hay un solo cuerpo, pero this toma lo que esté a la izquierda del punto en la línea que llama, así que sqlCourse.summary() arma su cadena con los valores de sqlCourse. Aunque tengas 300 cursos, lo único que se multiplica son las instancias que guardan title y hours.
Buscar en orden hasta llegar a null — la cadena de prototipos
Si creas un curso en vivo con class LiveCourse extends Course, liveCourse.summary() también funciona. Pero el único método escrito en el prototipo de liveCourse, LiveCourse.prototype, es schedule.
Los nombres se buscan a lo largo de la cadena de prototipos (la secuencia de referencias que obtienes al seguir el prototipo del prototipo hasta llegar a null), empezando por la instancia. extends hace que el siguiente eslabón después de LiveCourse.prototype sea Course.prototype, y después viene Object.prototype (un objeto integrado que está al final de la cadena).
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} horas)`; }
}
class LiveCourse extends Course { schedule() { return "Los martes a las 20:00"; } }
const liveCourse = new LiveCourse("React en la práctica", 10);
// Obtiene los prototipos nivel por nivel
const step1 = Object.getPrototypeOf(liveCourse);
const step2 = Object.getPrototypeOf(step1);
const step3 = Object.getPrototypeOf(step2);
console.log(step1 === LiveCourse.prototype); // true
console.log(step2 === Course.prototype); // true
console.log(step3 === Object.prototype); // true
console.log(Object.getPrototypeOf(step3)); // null (el final de la cadena)
// summary se encuentra en el segundo nivel, Course.prototype
console.log(liveCourse.summary()); // React en la práctica (10 horas)
En la cadena está Course.prototype, no Course en sí. Los métodos marcados como static viven en Course, así que si lees uno desde una instancia, la búsqueda llega hasta null sin encontrarlo y da undefined; si lo llamas, se lanza un TypeError.
Una clave con el mismo nombre en la instancia se encuentra primero
Si asignas liveCourse.summary = "Próximamente";, el nombre se encuentra en liveCourse mismo y el prototipo nunca se revisa. Entonces liveCourse.summary() lanza TypeError: liveCourse.summary is not a function.
Revisar solo las claves propias de la instancia — hasOwnProperty
Supón que recibes datos de cursos y quieres comprobar si summary se sobrescribió con una asignación directa en algún curso. "summary" in jsCourse da true incluso para un método del prototipo, así que no te dice si el valor se asignó a la instancia misma.
hasOwnProperty (un método que devuelve true solo cuando el objeto mismo tiene la clave) recibe la clave como cadena, como en jsCourse.hasOwnProperty("title"). El método en sí vive en Object.prototype y se encuentra a través de la cadena.
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} horas)`; }
}
const jsCourse = new Course("JS básico", 12);
const sqlCourse = new Course("SQL básico", 8);
sqlCourse.summary = "Próximamente"; // Se sobrescribe solo en este curso
// in da true incluso para los nombres del prototipo
console.log("summary" in jsCourse); // true
console.log("summary" in sqlCourse); // true
// hasOwnProperty da true solo para las claves que tiene el curso mismo
console.log(jsCourse.hasOwnProperty("summary")); // false
console.log(sqlCourse.hasOwnProperty("summary")); // true (se sobrescribió)
console.log(Course.prototype.hasOwnProperty("summary")); // true
title—"JS básico"hours—12
summary()—trueconin,falseconjsCourse.hasOwnProperty
hasOwnProperty()—jsCoursepuede llamarlo porque se encuentra aquí
En sqlCourse, que se sobrescribió con una asignación, hasOwnProperty("summary") da true, así que basta una línea para encontrar solo los cursos sobrescritos. La tabla de abajo muestra cómo tratan title y summary otras formas de comprobarlo.
| Comprobación | title (propio de jsCourse) | summary (en Course.prototype) |
|---|---|---|
| ¿Lo lista Object.keys(jsCourse)? | Sí | No |
| ¿Lo lista for...in? | Sí | No |
| Course.prototype.hasOwnProperty("clave") | false | true |
Agregar métodos desde afuera — asignar a prototype
Ahora supón que las instancias de los cursos las crea el código compartido de otro equipo y te las pasa, así que no puedes cambiar ni el cuerpo de la clase ni las líneas con new. Si asignas a cada curso, uno por uno, una función que devuelva «Curso: nombre» para la pantalla, los cursos en los que olvides asignarla lanzan un TypeError al llamarla.
Si asignas una función a prototype, como en Course.prototype.label = function () { ... }, todas las instancias encuentran el mismo label a través de la cadena. Si puedes editar el cuerpo de la clase, escribe el método ahí.
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} horas)`; }
}
const jsCourse = new Course("JS básico", 12); // Primero se crean dos cursos
const sqlCourse = new Course("SQL básico", 8);
// Si solo lo asignas a jsCourse, sqlCourse se queda sin label
jsCourse.label = function () { return `Curso: ${this.title}`; };
console.log(typeof sqlCourse.label); // undefined (si lo llamas, lanza un TypeError)
// Se asigna una vez a Course.prototype (con function, porque lee this)
Course.prototype.label = function () { return `Curso: ${this.title}`; };
console.log(sqlCourse.label()); // Curso: SQL básico
console.log(sqlCourse.hasOwnProperty("label")); // false
// Reúne las claves que devuelve for...in
const keys = [];
for (const key in sqlCourse) keys.push(key);
console.log(keys.join(", ")); // title, hours, label
Se encuentra incluso en sqlCourse, que se creó antes de la asignación, porque la cadena se recorre en el momento en que se usa el nombre. A diferencia de summary, que está escrito en el cuerpo de la clase, un label agregado por asignación también aparece en for...in, así que el código que recorre las claves lo recibe mezclado con las claves de los datos.
Escribe con function las funciones que agregas a prototype
Si escribes Course.prototype.label = () => this.title;, la función flecha usa el this de fuera de la clase. En el editor de este sitio, ese valor es undefined, así que sqlCourse.label() lanza un TypeError. Usa function.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Con LiveCourse extends Course, ¿cuál es el prototipo de LiveCourse.prototype?
Pregunta 3Ya existen algunos cursos. ¿Qué pasa si luego asignas a Course.prototype un label escrito con function?