Pregunta 1Vas a agregar PdfFile a una lista que tiene un bucle que llama a file.preview(). ¿Qué tienes que agregar?
Polimorfismo — la misma llamada, distinto comportamiento
Dale a cada clase un método con el mismo nombre y una sola llamada sirve para todas. Incluye arrays mixtos, instanceof y duck typing.
Imagina un formulario de contacto que lista sus archivos adjuntos y muestra una miniatura para las imágenes y un botón de reproducción para los videos. Si guardas el tipo como cadena y lo separas con if en la función que muestra cada adjunto, tendrás que abrir esa función y agregarle código cada vez que sumes un tipo.
Este artículo trata el polimorfismo, en el que un método con el mismo nombre ejecuta un código distinto en cada clase, y el duck typing, que no tiene en cuenta la herencia.
Llevar las diferencias de visualización a las clases — métodos con el mismo nombre
Supón que cada adjunto es un objeto como { kind: "video", fileName: "demo.mp4" } y que una función llamada previewOf compara kind. Para agregar los videos, abres previewOf, que también contiene la rama de las imágenes, y le agregas código.
Con el polimorfismo (dar a clases distintas un método con el mismo nombre, para que quien llama escriba siempre la misma llamada y se ejecute el código de la clase que creó la instancia), pones la visualización de cada tipo en el preview de su clase hija.
Una vez que cada clase hija sobrescribe el preview del padre, quien llama solo escribe preview(), sin comprobar de qué clase hija se trata.
// Guarda el tipo como cadena y lo separa con if en la función que lo muestra
function previewOf(file) {
if (file.kind === "image") return `${file.fileName}: Mostrar miniatura`;
return `${file.fileName}: Sin vista previa`;
}
console.log(previewOf({ kind: "video", fileName: "demo.mp4" })); // demo.mp4: Sin vista previa
// Escribe un método preview con el mismo nombre en la clase de cada tipo
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: Sin vista previa`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName}: Mostrar miniatura`; }
}
class VideoFile extends Attachment {
preview() { return `${this.fileName}: Mostrar botón de reproducción`; }
}
console.log(new ImageFile("logo.png").preview()); // logo.png: Mostrar miniatura
console.log(new VideoFile("demo.mp4").preview()); // demo.mp4: Mostrar botón de reproducción
El primer console.log mostró «Sin vista previa» porque previewOf todavía no tiene una rama para "video". Con clases, quien agrega los videos solo tiene que usar el mismo nombre, preview, y no necesita recordar qué cadenas van en kind.
Llamar a varios tipos a la vez — arrays y for...of
La lista llega como un solo array que mezcla imágenes, PDF y archivos de tipo desconocido. Si guardas cada uno en su propia variable y escribes una llamada a preview() para cada uno, tendrás que agregar o quitar llamadas cada vez que cambie la cantidad de adjuntos.
Puedes poner instancias de clases distintas en un mismo array. Si lo recorres con for...of, el file.preview() del cuerpo del bucle se ejecuta en cada vuelta sobre una instancia de una clase distinta, y la llamada sigue siendo una sola línea.
Cuando agregues PdfFile para los PDF, no tienes que tocar el bucle.
// Attachment e ImageFile se definen como en el primer ejemplo
// Un tipo agregado después. El bucle no cambia
class PdfFile extends Attachment {
preview() { return `${this.fileName}: Mostrar página 1`; }
}
const attachments = [
new ImageFile("logo.png"),
new PdfFile("manual.pdf"),
new Attachment("memo.txt"),
];
// El cuerpo es una línea. En cada vuelta, file contiene una instancia de otra clase
for (const file of attachments) {
console.log(file.preview());
}
// logo.png: Mostrar miniatura
// manual.pdf: Mostrar página 1
// memo.txt: Sin vista previa
memo.txt, en la tercera vuelta, se creó a partir de Attachment, así que se ejecuta el preview propio de Attachment. Si una clase hija nueva olvida definir preview, cae en esta misma salida en lugar de lanzar un error, así que cada vez que agregues un tipo, revisa su línea en la lista.
Llamar a código que solo tiene un tipo — separar con instanceof
Ahora, las filas de imágenes de la lista necesitan un botón para recortar. cropButton, que devuelve el botón, solo está definido en ImageFile, así que si llamas a file.cropButton() con todos los elementos en el bucle, se lanza un TypeError en la vuelta de memo.txt.
instanceof devuelve true si el valor de la izquierda se creó a partir de la clase de la derecha o de alguna de sus clases hijas. Comprueba file instanceof ImageFile y llama a cropButton solo en las vueltas en que dé true.
Como todas las clases tienen preview, puedes llamarlo directamente, sin comprobar nada.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: Sin vista previa`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName}: Mostrar miniatura`; }
cropButton() { return `${this.fileName}: Mostrar botón de recorte`; } // Solo lo tiene ImageFile
}
const attachments = [new ImageFile("logo.png"), new Attachment("memo.txt")];
for (const file of attachments) {
console.log(file.preview()); // Todas las clases lo tienen, así que se llama sin comprobar
if (file instanceof ImageFile) {
console.log(file.cropButton()); // Solo se llama en las vueltas de ImageFile
}
}
// logo.png: Mostrar miniatura
// logo.png: Mostrar botón de recorte
// memo.txt: Sin vista previa
La fila de arriba falla con TypeError: file.cropButton is not a function. Si separas con instanceof incluso para preview, terminas agregando una comprobación por cada tipo, igual que en previewOf. Reserva las ramas con instanceof para los métodos que solo tienen algunas clases.
Si usas !, pon instanceof entre paréntesis
En !file instanceof ImageFile, primero se evalúa !file, que da false, así que en realidad se comprueba false instanceof ImageFile, que siempre es false. Para decir «no es un ImageFile», ponlo entre paréntesis: !(file instanceof ImageFile).
Listar valores que no heredan — duck typing
La lista también tiene que mostrar enlaces a carpetas. Un enlace es un objeto literal con un método preview() y no hereda de Attachment. Si compruebas instanceof Attachment para descartar datos viejos sin preview, como { fileName: "old.doc" }, el enlace también da false y se muestra como «No se puede mostrar», igual que los datos viejos.
Con el duck typing (decidir cómo tratar un valor solo según si tiene los métodos que necesitas, sin comprobar su clase ni su herencia), compruebas typeof item.preview === "function".
La llamada a un método funciona sea cual sea la clase, siempre que el valor a la izquierda del punto tenga una función con ese nombre.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: Sin vista previa`; }
}
// Un enlace creado sin clase. No hereda de Attachment
const folderLink = {
fileName: "Cotizaciones",
preview() { return `${this.fileName}: Abrir enlace`; },
};
console.log(folderLink instanceof Attachment); // false
// Decide según si tiene preview como función, no según su clase
for (const item of [new Attachment("memo.txt"), folderLink, { fileName: "old.doc" }]) {
if (typeof item.preview === "function") {
console.log(item.preview()); // memo.txt: Sin vista previa / Cotizaciones: Abrir enlace
} else {
console.log(`${item.fileName}: No se puede mostrar`); // old.doc: No se puede mostrar
}
}
Como no hay comprobación con instanceof, también pasa un valor cuya función preview no devuelve una cadena. Asegúrate de que todos los valores que pongas en la lista tengan un preview() que devuelva una cadena para mostrar. La tabla de abajo compara las tres formas de llamar que usaste en este artículo.
| Cómo se llama | Qué comprueba | Cuándo usarla |
|---|---|---|
| Llamar a preview() directamente | Nada | Todos los elementos tienen preview |
| Separar con instanceof | ¿Viene de ImageFile o de una hija? | Para un método que solo tiene esa clase |
| Separar con typeof | ¿preview es una función? | Para aceptar cualquier valor con preview |
in da true aunque preview no sea una función
"preview" in item da true aunque el valor sea una cadena, como en { preview: "Próximamente" }. Si llamas a item.preview() con ese valor, se lanza TypeError: item.preview is not a function, así que antes de llamarlo, comprueba con typeof que de verdad sea una función.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Si memo se creó a partir de Attachment, ¿cuál es el resultado de !memo instanceof ImageFile?
Pregunta 3Para un objeto literal link con un método preview(), ¿qué dan link instanceof Attachment y typeof link.preview?