Aprende leyendo en orden

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 código que abres para agregar la vista previa de videos
Agregar la vistaprevia de videosAbrirpreviewOfEditar la funciónque tiene la ramade imágenesLas imágenestambién puedencambiarAgregar la vistaprevia de videosEscribirclass VideoFileImageFileno se tocaLas imágenes seven igual
La fila de arriba modifica previewOf, la misma función que tiene la rama de las imágenes. La fila de abajo solo agrega VideoFile y nunca toca ImageFile.

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.

En una pantalla de pago, vas a mostrar el importe a pagar con cada método de pago, comisión incluida. PaymentMethod ya está declarado.

① Define una clase para el pago en tienda de conveniencia que herede de PaymentMethod.

② Del mismo modo, define una clase para el pago contra entrega.

③ Crea una tarjeta de crédito, un pago en tienda de conveniencia y un pago contra entrega, y muestra el importe a pagar para 4980 yenes.

④ Muestra el importe a pagar para 12800 yenes con el pago contra entrega.

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

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
Qué preview se ejecuta en cada vuelta
Cuerpo de for...offile.preview()Vuelta 1logo.pngVuelta 2manual.pdfVuelta 3memo.txtpreview deImageFilepreview dePdfFilepreview deAttachmentlogo.png: Mostrarminiaturamanual.pdf:Mostrar página 1memo.txt: Sinvista previa
La llamada del bucle es una sola línea, pero en cada vuelta se ejecuta un cuerpo distinto. Qué preview se ejecuta depende de la clase que creó el file de esa vuelta.

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.

En una pantalla de confirmación de pedido, vas a sugerir el método de pago con el menor importe a pagar para cada total. PaymentMethod, BankTransfer, PayLater y orderTotals ya están declarados.

① Define una clase para el pago a plazos que sume el 3% del precio como comisión.

② Pon los tres métodos de pago en un mismo array.

③ Para cada total del pedido, elige el método de pago con el menor importe a pagar.

④ Muestra el método que elegiste y su importe a pagar.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
En qué vueltas se llama a cropButton
file.cropButton()con todosEn la vuelta 2,file es memo.txtAttachment notiene cropButtonLanza unTypeErrorSeparar coninstanceofImageFilefalse en lavuelta de memo.txtAvanza sin llamara cropButtonSolo logo.pngtiene botón
La fila de arriba falla en la vuelta de memo.txt, que no tiene cropButton. La fila de abajo lo llama solo en las vueltas de ImageFile.

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).

En un pedido que se puede pagar con puntos, vas a agregar los puntos restantes solo a la fila del pago con puntos. PaymentMethod, PointPayment, methods y cartTotal ya están declarados.

① Recorre los métodos de pago en orden y muestra el importe a pagar con cada uno.

② Solo en la fila del pago con puntos, agrega al final los puntos que quedan después de pagar.

③ Muestra cuántos métodos de pago no son de puntos.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
  }
}
Cómo trata la lista a folderLink
instanceofAttachmentNo se creódesde una claseinstanceofda falseCotizaciones: Nose puede mostrartypeofitem.previewTiene la funciónpreviewtypeof da"function"Cotizaciones:Abrir enlace
El mismo folderLink se muestra distinto según cómo lo compruebes. typeof ignora la clase y solo comprueba si preview es una función.

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 llamaQué compruebaCuándo usarla
Llamar a preview() directamenteNadaTodos 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.

QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Vas a agregar PdfFile a una lista que tiene un bucle que llama a file.preview(). ¿Qué tienes que agregar?

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?