Aplikasi Budi menyimpan catatan sementara untuk setiap pengguna yang sedang login: berapa kali percobaan kata sandi gagal, dan data sesi kecil lainnya. Penampung yang paling dekat adalah Map dengan object pengguna sebagai kunci — kebetulan persis dengan yang dipelajari di chapter Map. Persoalan muncul ketika pengguna menutup aplikasinya: selama Map itu masih hidup, object pengguna yang sudah tidak dipakai bagian lain dari program tetap tertahan di memori, karena Map masih menautkan object tersebut sebagai kunci.

WeakMap dan WeakSet menyelesaikan persoalan ini dengan referensi lemah (weak reference): penautan ke object yang tidak menghalangi proses pembersihan memori. Chapter ini membandingkan kunci kuat milik Map dengan kunci lemah milik WeakMap, mempraktikkannya pada catatan sesi Budi, lalu menegaskan batasan yang sengaja dirancang untuk kedua struktur ini.

Masalah: Kunci Kuat pada Map #

Aturan Map: setiap kunci ditautkan secara kuat (strong reference). Di balik layar, runtime JavaScript menjalankan garbage collection — proses berkala yang mengosongkan memori dari object yang tidak lagi dirujuk siapa pun. Pertanyaan yang dijawab proses ini sederhana: apakah object masih dapat dijangkau dari kode yang hidup? Selama sebuah object masih menjadi kunci sebuah Map yang hidup, jalur penjangkauan itu tetap ada, sehingga garbage collection tidak akan mengambilnya, seberapa lama pun program berjalan.

Konsekuensinya terasa pada data yang jumlahnya berubah-ubah. Catatan sesi untuk seribu pengguna akan tetap tersimpan di Map meskipun seribu object penggunanya sudah selesai dipakai — memori tidak bisa dilepaskan karena kuncinya masih kuat. Untuk data yang seharusnya ikut hilang ketika pemiliknya selesai, Map adalah pilihan yang terlalu berpegang teguh.

Referensi Lemah: Cara Kerja WeakMap #

Aturan WeakMap: kunci hanya boleh object, dan penautannya lemah. Referensi lemah tidak dihitung oleh garbage collection — bila tidak ada bagian program lain yang memegang object itu, runtime boleh mengambil memorinya, dan entri di WeakMap ikut dibersihkan tanpa satu baris kode pembersihan pun.

Dua akibat penting mengikuti aturan ini. Pertama, waktu pembersihan tidak bisa diprediksi: garbage collection berjalan sesuai keputusan engine, dan program tidak diberi tahu kapan entri dibersihkan. Kedua, isi WeakMap tidak boleh diandalkan sebagai data yang bisa ditelusuri — kapan pun kita menghitung isinya, angkanya bisa berubah sendiri. Kedua akibat ini yang menjadi alasan di balik batasan API pada bagian akhir chapter.

Kapan WeakMap dan WeakSet Dipakai #

Pemakaian yang lazim berputar pada satu ide: metadata yang siklus hidupnya mengikuti object pemiliknya. Contoh konkret:

  • Hitungan sementara per object — percobaan login, penghitung penggunaan, penanda waktu akses terakhir.
  • Cache hasil perhitungan per object — hasil yang boleh dibuang bersama objectnya tanpa baris pembersihan.
  • Penanda “sudah diproses” — WeakSet menandai object yang sudah dilewati suatu tahap, tanpa mengubah object itu sendiri.
  • Data tambahan pada object pihak lain — misalnya elemen halaman yang dikelola browser; materi ini kembali terasa saat section DOM.

Keempat pola di atas tidak butuh penelusuran isi, tidak butuh penghitungan entri, dan tidak boleh menahan object pemiliknya — tiga syarat yang persis cocok dengan desain WeakMap dan WeakSet.

WeakMap dalam Praktik #

Function catatPercobaan berikut menempelkan hitungan percobaan login pada object pengguna. WeakMap dipakai sebagaimana Map — get, set, has, delete — hanya dengan jaminan tambahan: begitu object pengguna tidak lagi dipakai di mana pun, catatannya ikut dibereskan. Berkas yang sama juga memperlihatkan WeakSet untuk menandai laporan yang sudah diproses:

// catatan-sesi.js
// Metadata sementara per pengguna: WeakMap tidak menahan object dari
// proses garbage collection.

const percobaanLogin = new WeakMap();

function catatPercobaan(user) {
  const catatan = percobaanLogin.get(user) || { jumlah: 0 };
  catatan.jumlah += 1;
  percobaanLogin.set(user, catatan);
  return catatan.jumlah;
}

const budi = { nama: "Budi", email: "[email protected]" };
const siti = { nama: "Siti", email: "[email protected]" };

console.log(catatPercobaan(budi));
console.log(catatPercobaan(budi));
console.log(catatPercobaan(siti));
console.log(percobaanLogin.has(siti));
percobaanLogin.delete(siti);
console.log(percobaanLogin.has(siti));

// Kunci WeakMap wajib object; nilai primitive ditolak
try {
  percobaanLogin.set("[email protected]", { jumlah: 1 });
} catch (err) {
  console.log(err.name + ": " + err.message);
}

// WeakSet: menandai laporan yang sudah diproses
const diproses = new WeakSet();
const laporan1 = { nomor: 501 };
const laporan2 = { nomor: 502 };
diproses.add(laporan1);
console.log(diproses.has(laporan1));
console.log(diproses.has(laporan2));
console.log(typeof percobaanLogin.size);

Output yang Dihasilkan:

text
1
2
1
true
false
TypeError: Invalid value used as weak map key
true
false
undefined

Mari kita telaah baris-baris pentingnya:

  1. Baris 5 (const percobaanLogin = new WeakMap()): WeakMap dibuat tanpa argumen; pasangan diisi belakangan lewat function.
  2. Baris 8 (percobaanLogin.get(user) || { jumlah: 0 }): untuk pengguna baru, get mengembalikan undefined, dan operator || menggantinya dengan object hitungan awal — pola nilai cadangan dari section Operators.
  3. Baris 9–10 (catatan.jumlah += 1 lalu set): hitungan dinaikkan pada object catatan, lalu set menyimpannya kembali pada kunci object yang sama; pemanggilan kedua untuk Budi membaca catatan lama dan menghasilkan 2.
  4. Baris 20–22 (has dan delete): pemeriksaan keanggotaan berjalan per kunci object; setelah delete(siti), has(siti) mengembalikan false.
  5. Baris 26 (percobaanLogin.set("[email protected]", ...)): string ditolak sebagai kunci — runtime melempar TypeError: Invalid value used as weak map key. Blok try-catch menangkapnya agar program tetap jalan; pola penanganan error dibahas menyeluruh di section Error Handling.
  6. Baris 32–35 (WeakSet + add): WeakSet menandai laporan1 sebagai sudah diproses; laporan2 tidak pernah ditandai.
  7. Baris 36–37 (diproses.has(...)): pemeriksaan membedakan kedua object secara per-identitas — true untuk laporan yang ditandai, false untuk yang belum.
  8. Baris 38 (typeof percobaanLogin.size): hasilnya undefined — WeakMap tidak menyediakan size, dan tidak menyediakan cara untuk menghitung atau menelusuri isinya.

Batasan yang Disengaja #

WeakMap dan WeakSet sengaja dirancang tanpa fitur penelusuran. Tidak ada size, tidak ada keys() atau values(), tidak ada iterasi for...of, dan tidak ada clear(). Alasannya kembali ke referensi lemah: karena garbage collection boleh membersihkan entri kapan saja, menelusuri isi WeakMap berarti mengandalkan daftar yang isinya bisa berubah sendiri — sesuatu yang tidak berguna dan menyesatkan. Penelusuran yang benar-benar dibutuhkan adalah tugas Map dan Set.

Kunci juga dibandingkan per-identitas object, sama seperti Map. Object baru dengan isi serupa tidak akan menemukan catatan lama:

js
const kunci = { id: 1 };
const meta = new WeakMap();
meta.set(kunci, "data sesi");
console.log(meta.get(kunci));
console.log(meta.get({ id: 1 }));

Output yang Dihasilkan:

text
data sesi
undefined

Baris terakhir menghasilkan undefined karena object { id: 1 } pada baris kelima adalah object lain — identitasnya berbeda dari kunci, meskipun isinya sama. WeakSet menerima batasan serupa dengan pesan yang berbeda: add dengan nilai primitive seperti angka melempar TypeError: Invalid value used in weak set.

WeakMap dan WeakSet menutup keluarga koleksi kunci-nilai dan keanggotaan. Pertanyaan berikutnya lebih mendasar: mengapa Map, Set, array, dan string semuanya bisa ditelusuri for...of, sedangkan object biasa ditolak? Jawabannya ada pada protokol iterasi di chapter berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

catatan-sesi.js

Unduh berkas
javascript
// catatan-sesi.js
// Metadata sementara per pengguna: WeakMap tidak menahan object dari
// proses garbage collection.

const percobaanLogin = new WeakMap();

function catatPercobaan(user) {
  const catatan = percobaanLogin.get(user) || { jumlah: 0 };
  catatan.jumlah += 1;
  percobaanLogin.set(user, catatan);
  return catatan.jumlah;
}

const budi = { nama: "Budi", email: "[email protected]" };
const siti = { nama: "Siti", email: "[email protected]" };

console.log(catatPercobaan(budi));
console.log(catatPercobaan(budi));
console.log(catatPercobaan(siti));
console.log(percobaanLogin.has(siti));
percobaanLogin.delete(siti);
console.log(percobaanLogin.has(siti));

// Kunci WeakMap wajib object; nilai primitive ditolak
try {
  percobaanLogin.set("[email protected]", { jumlah: 1 });
} catch (err) {
  console.log(err.name + ": " + err.message);
}

// WeakSet: menandai laporan yang sudah diproses
const diproses = new WeakSet();
const laporan1 = { nomor: 501 };
const laporan2 = { nomor: 502 };
diproses.add(laporan1);
console.log(diproses.has(laporan1));
console.log(diproses.has(laporan2));
console.log(typeof percobaanLogin.size);