Belajar dengan membaca secara berurutan

Polymorphism — Pemanggilan Sama, Perilaku Berbeda

Polymorphism memberi tiap class method bernama sama agar satu bentuk pemanggilan cukup untuk semuanya. Membahas array campuran, instanceof, dan duck typing.

Bayangkan formulir kontak yang menampilkan daftar lampiran: thumbnail untuk gambar dan tombol play untuk video. Kalau jenisnya disimpan sebagai string lalu dicabangkan dengan if di function tampilan, kamu harus membuka function itu dan menambahkan kode setiap kali ada jenis baru.

Artikel ini membahas polymorphism, yaitu method bernama sama yang menjalankan kode berbeda di tiap class, dan duck typing, yang sama sekali tidak memedulikan hubungan inheritance.

Menaruh Perbedaan Tampilan di Class — Method dengan Nama yang Sama

Misalnya setiap lampiran berupa object seperti { kind: "video", fileName: "demo.mp4" }, dan function bernama previewOf membandingkan kind. Untuk menambahkan video, kamu harus membuka previewOf, yang juga berisi cabang untuk gambar, lalu menambahkan kode di sana.

Dengan polymorphism (memberi beberapa class method dengan nama yang sama, sehingga pemanggil menulis pemanggilan yang sama dan kode milik class yang membuat instance-lah yang berjalan), tampilan untuk tiap jenis ditulis di preview milik child class.

Kalau setiap child class sudah meng-override preview milik parent, pemanggil cukup menulis preview() tanpa perlu memeriksa child class mana yang sedang dipegangnya.

// Simpan jenisnya sebagai string, lalu cabangkan dengan if di function tampilan
function previewOf(file) {
  if (file.kind === "image") return `${file.fileName}: Tampilkan thumbnail`;
  return `${file.fileName}: Tidak ada preview`;
}
console.log(previewOf({ kind: "video", fileName: "demo.mp4" }));   // demo.mp4: Tidak ada preview

// Tulis method preview dengan nama yang sama di class tiap jenis
class Attachment {
  constructor(fileName) { this.fileName = fileName; }
  preview() { return `${this.fileName}: Tidak ada preview`; }
}
class ImageFile extends Attachment {
  preview() { return `${this.fileName}: Tampilkan thumbnail`; }
}
class VideoFile extends Attachment {
  preview() { return `${this.fileName}: Tampilkan tombol play`; }
}
console.log(new ImageFile("logo.png").preview());   // logo.png: Tampilkan thumbnail
console.log(new VideoFile("demo.mp4").preview());   // demo.mp4: Tampilkan tombol play
Kode yang Dibuka untuk Menambah Preview Video
Menambahpreview videoMembukapreviewOfMengedit functionyang berisicabang gambarPreview gambarbisa ikut berubahMenambahpreview videoMenulisclass VideoFileImageFiletidak disentuhPreview gambartetap sama
Baris atas mengubah previewOf, function yang sama dengan tempat cabang gambar. Baris bawah hanya menambah VideoFile dan tidak menyentuh ImageFile sama sekali.

console.log pertama menampilkan "Tidak ada preview" karena previewOf belum punya cabang untuk "video". Dengan class, siapa pun yang menambahkan video cukup memakai nama yang sama, preview, dan tidak perlu mengingat string apa saja yang masuk ke kind.

Di halaman checkout, tampilkan jumlah yang harus dibayar untuk tiap metode pembayaran, termasuk biayanya. PaymentMethod sudah dideklarasikan.

① Definisikan class untuk pembayaran di minimarket yang mewarisi PaymentMethod.

② Dengan cara yang sama, definisikan class untuk bayar di tempat.

③ Buat metode kartu kredit, minimarket, dan bayar di tempat, lalu tampilkan jumlah yang harus dibayar untuk 4980 yen.

④ Tampilkan jumlah yang harus dibayar untuk 12800 yen dengan bayar di tempat.

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Memanggil Jenis Campuran Sekaligus — Array dan for...of

Daftar lampiran datang sebagai satu array yang mencampur gambar, PDF, dan file yang jenisnya tidak diketahui. Kalau setiap file kamu masukkan ke variabelnya sendiri lalu menulis pemanggilan preview() satu per satu, kamu harus menambah atau mengurangi baris pemanggilan setiap kali jumlah lampirannya berubah.

Instance dari class yang berbeda-beda bisa dimasukkan begitu saja ke dalam satu array. Kalau array itu kamu loop dengan for...of, file.preview() di body loop berjalan pada instance dari class yang berbeda di setiap putaran, dan pemanggilannya tetap satu baris.

Saat menambahkan PdfFile untuk PDF pun, loop-nya tidak perlu diubah.

// Attachment dan ImageFile didefinisikan sama seperti di contoh pertama
// Jenis yang ditambahkan belakangan. Loop-nya tidak diubah
class PdfFile extends Attachment {
  preview() { return `${this.fileName}: Tampilkan halaman 1`; }
}

const attachments = [
  new ImageFile("logo.png"),
  new PdfFile("manual.pdf"),
  new Attachment("memo.txt"),
];

// Body-nya satu baris. Di setiap putaran, file berisi instance dari class yang berbeda
for (const file of attachments) {
  console.log(file.preview());
}
// logo.png: Tampilkan thumbnail
// manual.pdf: Tampilkan halaman 1
// memo.txt: Tidak ada preview
preview Mana yang Berjalan di Tiap Putaran
Body for...offile.preview()Putaran 1logo.pngPutaran 2manual.pdfPutaran 3memo.txtpreview milikImageFilepreview milikPdfFilepreview milikAttachmentlogo.png:Tampilkanthumbnailmanual.pdf:Tampilkanhalaman 1memo.txt:Tidak ada preview
Pemanggilan di loop hanya satu baris, tapi body method yang berjalan berbeda di tiap putaran. preview yang berjalan ditentukan oleh class yang membuat file di putaran itu.

memo.txt di putaran ketiga dibuat dari Attachment, jadi preview milik Attachment sendiri yang berjalan. Kalau child class baru lupa mendefinisikan preview, hasilnya diam-diam memakai tampilan bawaan ini tanpa error, jadi setiap kali menambah jenis, cek barisnya di daftar.

Di halaman konfirmasi pesanan, sarankan metode pembayaran dengan jumlah bayar paling murah untuk tiap total pesanan. PaymentMethod, BankTransfer, PayLater, dan orderTotals sudah dideklarasikan.

① Definisikan class untuk cicilan yang menambahkan 3% dari harga sebagai biaya.

② Masukkan ketiga metode pembayaran ke dalam satu array.

③ Untuk tiap total pesanan, pilih metode pembayaran dengan jumlah bayar paling murah.

④ Tampilkan metode pembayaran yang dipilih beserta jumlah bayarnya.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Memanggil Kode yang Hanya Dimiliki Satu Jenis — Percabangan dengan instanceof

Berikutnya, baris gambar di daftar butuh tombol crop. cropButton, yang mengembalikan tombol itu, hanya didefinisikan di ImageFile, jadi memanggil file.cropButton() untuk semua item di loop akan melempar TypeError di putaran memo.txt.

instanceof mengembalikan true kalau value di sebelah kiri dibuat dari class di sebelah kanan atau salah satu child class-nya. Cek file instanceof ImageFile, lalu panggil cropButton hanya di putaran yang hasilnya true.

Karena semua class punya preview, method itu bisa langsung dipanggil tanpa pengecekan.

class Attachment {
  constructor(fileName) { this.fileName = fileName; }
  preview() { return `${this.fileName}: Tidak ada preview`; }
}
class ImageFile extends Attachment {
  preview() { return `${this.fileName}: Tampilkan thumbnail`; }
  cropButton() { return `${this.fileName}: Tampilkan tombol crop`; }   // Hanya ImageFile yang punya
}

const attachments = [new ImageFile("logo.png"), new Attachment("memo.txt")];

for (const file of attachments) {
  console.log(file.preview());          // Semua class punya, jadi panggil tanpa pengecekan
  if (file instanceof ImageFile) {
    console.log(file.cropButton());     // Panggil hanya di putaran ImageFile
  }
}
// logo.png: Tampilkan thumbnail
// logo.png: Tampilkan tombol crop
// memo.txt: Tidak ada preview
Menentukan Putaran yang Memanggil cropButton
file.cropButton()untuk semua itemfile di putaran 2adalah memo.txtAttachment tidakpunya cropButtonMelemparTypeErrorCabangkan denganinstanceofImageFilefalse di putaranmemo.txtLanjut tanpamemanggilcropButtonHanya logo.pngyang dapat tombol
Baris atas berhenti dengan error di putaran memo.txt, yang tidak punya cropButton. Baris bawah memanggilnya hanya di putaran ImageFile.

Baris atas gagal dengan TypeError: file.cropButton is not a function. Kalau preview pun kamu cabangkan dengan instanceof, pengecekannya akan bertambah untuk setiap jenis, sama seperti previewOf. Batasi percabangan instanceof hanya untuk method yang dimiliki sebagian class saja.

Bungkus instanceof dengan Kurung Saat Memakai !

Pada !file instanceof ImageFile, !file dievaluasi lebih dulu dan menjadi false, jadi yang sebenarnya dicek adalah false instanceof ImageFile, yang selalu false. Untuk menyatakan "bukan ImageFile", bungkus dengan kurung: !(file instanceof ImageFile).

Untuk pesanan yang bisa dibayar dengan poin, tambahkan sisa poin hanya di baris pembayaran poin. PaymentMethod, PointPayment, methods, dan cartTotal sudah dideklarasikan.

① Proses metode pembayaran secara berurutan, lalu tampilkan jumlah yang harus dibayar untuk masing-masing.

② Hanya di baris pembayaran poin, tambahkan sisa poin setelah dipakai di akhir baris.

③ Tampilkan jumlah metode pembayaran selain poin.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Menampilkan Value yang Tidak Mewarisi Class — Duck Typing

Daftar itu juga perlu menampilkan link folder. Link berupa object literal yang punya method preview(), dan tidak mewarisi Attachment. Kalau kamu mengecek instanceof Attachment untuk menyaring data lama yang tidak punya preview, seperti { fileName: "old.doc" }, link-nya juga mendapat false dan ikut masuk ke kelompok "Tidak bisa ditampilkan" bersama data lama.

Dengan duck typing (menentukan cara menangani sebuah value hanya dari ada atau tidaknya method yang dibutuhkan, tanpa memeriksa class atau hubungan inheritance-nya), pengecekannya memakai typeof item.preview === "function".

Pemanggilan method tetap berjalan apa pun class-nya, selama value di sebelah kiri titik punya function dengan nama itu.

class Attachment {
  constructor(fileName) { this.fileName = fileName; }
  preview() { return `${this.fileName}: Tidak ada preview`; }
}

// Link yang dibuat tanpa class. Tidak mewarisi Attachment
const folderLink = {
  fileName: "Folder penawaran",
  preview() { return `${this.fileName}: Buka link`; },
};
console.log(folderLink instanceof Attachment);   // false

// Tentukan dari ada atau tidaknya preview sebagai function, bukan dari class-nya
for (const item of [new Attachment("memo.txt"), folderLink, { fileName: "old.doc" }]) {
  if (typeof item.preview === "function") {
    console.log(item.preview());                   // memo.txt: Tidak ada preview / Folder penawaran: Buka link
  } else {
    console.log(`${item.fileName}: Tidak bisa ditampilkan`);   // old.doc: Tidak bisa ditampilkan
  }
}
Cara Daftar Menangani folderLink
instanceofAttachmentTidak dibuatdari classinstanceofbernilai falseFolder penawaran:Tidak bisaditampilkantypeofitem.previewPunya functionpreviewtypeof bernilai"function"Folder penawaran:Buka link
folderLink yang sama ditampilkan berbeda, tergantung cara pengecekannya. typeof tidak melihat class, hanya memeriksa apakah preview berupa function.

Karena tidak ada pengecekan instanceof, value yang function preview-nya tidak mengembalikan string juga ikut lolos. Pastikan setiap value yang dimasukkan ke daftar punya preview() yang mengembalikan string untuk ditampilkan. Tabel di bawah membandingkan tiga cara pemanggilan yang dipakai di artikel ini.

Cara memanggilYang dicekKapan dipakai
Panggil preview() langsungTidak adaSemua element punya preview
Cabang dengan instanceofDibuat dari ImageFile atau child-nya?Memanggil method khusus class itu
Cabang dengan typeofApakah preview sebuah function?Menerima semua value dengan preview

in Bernilai true Meskipun preview Bukan Function

"preview" in item bernilai true meskipun value-nya berupa string, seperti pada { preview: "Segera hadir" }. Memanggil item.preview() pada value itu melempar TypeError: item.preview is not a function, jadi sebelum memanggilnya, pakai typeof untuk memastikan value itu memang function.

QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Kamu menambahkan PdfFile ke daftar yang punya loop pemanggil file.preview(). Apa yang perlu kamu tambahkan?

Soal 2Untuk memo yang dibuat dari Attachment, apa hasil !memo instanceof ImageFile?

Soal 3Untuk object literal link yang punya method preview(), apa hasil link instanceof Attachment dan typeof link.preview?