Aprende leyendo en orden

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)
A qué apuntan una clase y sus instancias
new Course("JS básico", 12)class Coursenew Course("SQL básico", 8)jsCourseCourse.prototypetiene summarysqlCourseClaves propias:title, hoursUn solo cuerpode summaryNo es suyo, asíque busca enCourse.prototypejsCourse.prototypees undefinedLos dos summaryson el mismosummary() daSQL básico(8 horas)
El prototipo de los dos cursos es el mismo que el prototype de la clase. summary, que ellos no tienen, se encuentra ahí.

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)
El orden en que se busca summary
liveCourse(instancia)Tiene title, hoursNo tiene summaryLiveCourse.prototypeTiene scheduleNo tiene summaryCourse.prototypeTiene summarySe encuentra aquíObject.prototypeTiene métodosintegrados(después, null)
summary se busca desde arriba y se encuentra en Course.prototype. La cadena termina en null, justo después de Object.prototype.

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.

Vas a mostrar el orden de búsqueda de métodos para cada tipo de auto. Car, ElectricCar, compactCar, ev y prototypeNames ya están declarados.

① Muestra si el prototipo de ev es ElectricCar.prototype.

② Define chainOf, que une los nombres desde el prototipo del auto que recibe hasta justo antes de null.

③ Muestra los resultados de compactCar y ev.

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
Dónde buscan in y hasOwnProperty
Lo que revisa "summary" in jsCourse (toda la cadena)
jsCourse mismo (lo que revisa hasOwnProperty)
  • title"JS básico"
  • hours12
Course.prototype
  • summary()true con in, false con jsCourse.hasOwnProperty
Object.prototype
  • hasOwnProperty()jsCourse puede llamarlo porque se encuentra aquí
in mira toda la cadena, mientras que hasOwnProperty solo mira dentro del recuadro de jsCourse. summary no está en el recuadro de jsCourse.

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óntitle (propio de jsCourse)summary (en Course.prototype)
¿Lo lista Object.keys(jsCourse)?No
¿Lo lista for...in?No
Course.prototype.hasOwnProperty("clave")falsetrue

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
Dónde asignas label cambia el resultado
label asignadosolo a jsCoursesqlCourse no tienelabel propioTampoco está en lacadena: undefinedsqlCourse.label()lanza TypeErrorSe asigna aCourse.prototype.labelsqlCourse no tienelabel propioSe encuentra enCourse.prototypeCurso: SQL básico
En los dos casos, sqlCourse no tiene label propio. Solo se encuentra a través de la cadena cuando lo asignas a Course.prototype.

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.

Vas a agregar la duración a una lista de películas sin cambiar el cuerpo de Video. Video y videos ya están declarados.

① Agrega durationLabel, que todas las películas pueden llamar y que devuelve «N h N min».

② Para cada película, muestra «Título: N h N min».

③ Muestra si la primera película tiene durationLabel como propio.

④ Recorre la primera película con for...in y muestra las claves que la película no tiene como propias.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Para un método summary escrito en una clase, ¿qué devuelve jsCourse.hasOwnProperty("summary")?

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?