Soal 1Apa hasil instanceof Document untuk minutes, yang komponennya disalin dengan Object.assign?
Composition vs Inheritance — Object.assign dan Mixin
Fitur dipecah menjadi komponen, lalu digabungkan ke satu object. Artikel ini menjelaskan Object.assign, function yang mengembalikan komponen, mixin function, dan kapan sebaiknya memakai inheritance.
Di sistem dokumen kantormu, notulen rapat perlu fitur ekspor PDF, kontrak perlu fitur persetujuan, dan penawaran harga perlu keduanya. Di belakang extends, sebuah class hanya bisa menyebut satu parent, sehingga kamu terpaksa menulis satu class untuk setiap kombinasi fitur.
Artikel ini membahas composition, yang memecah fitur menjadi komponen terpisah lalu menambahkannya ke object, dan mixin function, yang menumpuk fitur ke sebuah class.
Menambahkan Hanya Komponen yang Dibutuhkan Setiap Dokumen — Object.assign
Misalkan kamu sudah membuat class ApprovableDocument yang mendukung persetujuan. Karena penawaran harga juga butuh ekspor PDF, kamu membuat class ExportableDocument yang meng-extend-nya. Notulen rapat hanya butuh ekspor PDF, jadi notulen terpaksa ikut mewarisi fitur persetujuan yang tidak pernah dipakai, atau meng-extend Document sendiri lalu menyalin kode PDF-nya.
Dengan composition (menyusun object dari beberapa kelompok method yang terpisah, satu kelompok per fitur), kamu memberikan komponen-komponen itu ke Object.assign(target, komponen1, komponen2). Function ini menyalin property dari argument kedua dan seterusnya ke argument pertama, lalu mengembalikan argument pertama itu.
class Document {
constructor(title) { this.title = title; }
}
// Buat satu kelompok method (komponen) untuk setiap fitur
const approvable = {
approve(name) { return `${this.title}: disetujui oleh ${name}`; },
};
const exportable = {
toPdfName() { return `${this.title}.pdf`; },
};
// Salin hanya komponen yang dibutuhkan setiap dokumen
const minutes = Object.assign(new Document("Notulen rapat mingguan"), exportable);
const estimate = Object.assign(new Document("Penawaran servis"), approvable, exportable);
console.log(Object.keys(estimate).join(", ")); // title, approve, toPdfName
console.log(estimate.approve("Budi")); // Penawaran servis: disetujui oleh Budi
console.log(minutes.toPdfName()); // Notulen rapat mingguan.pdf
console.log(typeof minutes.approve); // undefined (notulen tidak punya fitur persetujuan)
Pada estimate.approve("Budi"), object di sebelah kiri titik adalah estimate, jadi this di dalam approve adalah estimate, dan this.title adalah judul penawaran itu. Target-nya tetap instance hasil new Document, jadi instanceof Document juga bernilai true.
Daftar Komentar Terpisah untuk Setiap Dokumen — Function yang Mengembalikan Komponen
Sekarang kamu juga ingin daftar komentar, jadi kamu menjadikan { comments: [], addComment(text) { ... } } sebagai komponen dan menyalinnya ke notulen dan penawaran dengan Object.assign. Yang disalin hanya reference ke array-nya, jadi comments di kedua dokumen menunjuk ke array yang sama, dan komentar yang ditambahkan ke notulen juga muncul di daftar komentar penawaran.
Kalau komponen komentar kamu tulis ulang sebagai commentable, yaitu function yang membuat dan mengembalikan komponen baru setiap kali dipanggil, comments mendapat array [] baru di setiap pemanggilan. Jadikan approvable function juga, lalu tulis { ...approvable(), ...commentable() }: spread syntax menyalin key dan value setiap komponen ke object baru.
// Function yang mengembalikan komponen: setiap pemanggilan membuat object dan array baru
const commentable = () => ({
comments: [],
addComment(text) { this.comments.push(text); },
});
const approvable = () => ({
approvedBy: "belum disetujui",
approve(name) { this.approvedBy = name; },
});
// Susun komponen dengan spread syntax untuk membuat dokumen baru
const minutes = { title: "Notulen rapat mingguan", ...commentable() };
const estimate = { title: "Penawaran servis", ...approvable(), ...commentable() };
minutes.addComment("Lanjut tgl 10");
estimate.approve("Budi");
console.log(minutes.comments.length, estimate.comments.length); // 1 0 (array-nya terpisah)
console.log(estimate.approvedBy); // Budi
console.log(Object.keys(estimate).join(", ")); // title, approvedBy, approve, comments, addComment
Yang membuat object baru adalah { }; spread syntax hanya menyalin key dari return value setiap komponen ke dalamnya. Berbeda dengan Object.assign, kamu tidak menyiapkan target lebih dulu, jadi hasilnya adalah object biasa, bukan instance dari sebuah class.
Kalau Ada Key yang Sama, Komponen Terakhir yang Dipakai
Baik dengan Object.assign maupun spread syntax, kalau dua komponen punya nama key yang sama, yang tersisa adalah value dari komponen yang diberikan paling akhir. Kalau approvable() dan commentable() sama-sama punya describe, hanya yang terakhir yang dipakai dan tidak ada error yang muncul, jadi pastikan nama method tidak bentrok antarkomponen.
Menumpuk Fitur di Definisi Class — Mixin Function
Di bagian lain sistem, penawaran didefinisikan sebagai class Estimate dan dibuat dengan new Estimate(...) di setiap layar. Kalau fitur persetujuan dan komentar kamu tambahkan belakangan dengan Object.assign, kamu harus menyalinnya setelah setiap new, dan penawaran yang terlewat akan melempar TypeError saat approve dipanggil.
Mixin function (function yang menerima class dan mengembalikan class yang meng-extend class itu dengan fitur tambahan) ditulis sebagai (Base) => class extends Base { ... }. class extends Base { ... } adalah class expression tanpa nama. Kamu bisa menulis pemanggilan function ini setelah extends, dan class yang dikembalikannya menjadi parent.
class Document {
constructor(title) { this.title = title; }
describe() { return this.title; }
}
// Menerima class dan mengembalikan class yang meng-extend-nya dengan fitur tambahan
const Approvable = (Base) => class extends Base {
approve(name) { this.approvedBy = name; }
describe() { return `${super.describe()} (${this.approvedBy ?? "belum disetujui"})`; }
};
const Commentable = (Base) => class extends Base {
comments = []; // Array baru untuk setiap instance
addComment(text) { this.comments.push(text); }
};
// Keduanya ditumpuk di definisi class, jadi tidak ada yang perlu disalin setelah setiap new
class Estimate extends Commentable(Approvable(Document)) {}
const estimate = new Estimate("Penawaran servis");
estimate.approve("Budi");
console.log(estimate.describe()); // Penawaran servis (Budi)
Di Approvable, super.describe() memanggil describe milik Document, yaitu Base yang diterimanya. Kalau urutan tumpukannya kamu tukar menjadi Approvable(Commentable(Document)), Estimate tetap mendapat fitur persetujuan dan komentar.
Meng-extend Mixin Tanpa Memanggilnya Melempar TypeError
extends Approvable menjadikan arrow function itu sendiri sebagai parent. Arrow function tidak bisa menjadi parent sebuah class, jadi JavaScript melempar TypeError: Class extends value ... is not a constructor or null. Tulis Approvable(Document) sebagai parent-nya.
Membandingkan Dua Pendekatan — Mixin vs extends
Kalau kamu menulis mixin function untuk setiap fitur, tanda kurungnya mulai bersarang, seperti class Estimate extends Commentable(Approvable(Document)) {}. Makin banyak yang ditumpuk, makin sulit menangkap dari satu baris deklarasi itu apa sebenarnya Estimate dan fitur apa saja yang ditambahkan padanya.
Bandingkan dengan versi inheritance: CommentableDocument (komentar) meng-extend ApprovableDocument (persetujuan). Dengan kedua pendekatan, instanceof Document bernilai true dan kamu bisa memakai super. Bedanya ada pada apakah kamu bisa memilih fitur untuk setiap class.
class Document { constructor(title) { this.title = title; } }
const Commentable = (Base) => class extends Base { // Mixin yang sama dengan bagian sebelumnya
comments = []; addComment(text) { this.comments.push(text); }
};
// Inheritance: class komentar dibangun di atas class persetujuan
class ApprovableDocument extends Document {
approve(name) { this.approvedBy = name; }
}
class CommentableDocument extends ApprovableDocument {
comments = [];
addComment(text) { this.comments.push(text); }
}
const oldMinutes = new CommentableDocument("Notulen rapat mingguan");
console.log(typeof oldMinutes.approve); // function (ikut mewarisi persetujuan yang tidak dipakai)
// Menumpuk dengan mixin
class Minutes extends Commentable(Document) {}
const minutes = new Minutes("Notulen rapat mingguan");
console.log(typeof minutes.approve); // undefined (hanya fitur komentar yang ditambahkan)
console.log(minutes instanceof Document, oldMinutes instanceof Document); // true true
Fitur yang dibutuhkan sebagian jenis object tetapi tidak dibutuhkan yang lain, seperti persetujuan dan komentar, sebaiknya dijadikan komponen. Pakai extends untuk hubungan yang cocok dengan satu garis parent-child, misalnya "penawaran adalah salah satu jenis dokumen". Tabel di bawah merangkum empat pendekatan yang dipakai di artikel ini.
| Pendekatan | Cara fitur ditambahkan | Cocok untuk |
|---|---|---|
| extends Parent | Warisi semua dari parent | Hubungan "jenis dari" |
| Object.assign(target, komponen) | Salin ke object yang ada | Tambah fitur ke 1 object |
| { ...komponen() } | Salin ke object baru | Susun tanpa class |
| extends Mixin(Parent) | Tumpuk fitur pilihan | Tambah di definisi class |
Cek Pemahaman
Jawab setiap pertanyaan satu per satu.
Soal 2Kalau comments: [] dari satu komponen kamu salin ke dua dokumen dengan Object.assign, lalu kamu push ke salah satunya, apa yang terjadi?
Soal 3Pada Commentable(Approvable(Document)), class mana yang langsung meng-extend Document?