1 September 2021

WeakMap dan WeakSet

Seperti yang kita tahu dari bab Pengumpulan sampah (_Garbage collection_), Mesin Javascript menyimpan sebuah nilai didalam memori selama itu bisa terjangkau (dan secara potensial bisa digunakan).

Contoh:

let john = { name: "John" };

// Objeknya bisa diakses, john mereferensi kedalamnya.

// tulis urang referensinya
john = null;

// objeknya akan dihilangkan dari memori

Biasanya. properti dari sebuah objek atau elemen dari array atau struktur data lainnya bisa dianggap bisa dijangkau dan tetap berada dimemori selama struktur datanya masih didalam memori.

Contoh, jika kita memasukan objek kedalam sebuah array, lalu selama arraynya ada, objeknya akan tetap ada juga, bahkan jika disana sudah tidaka ada yang mereferensi kedalamnya.

Seperti ini:

let john = { name: "John" };

let array = [ john ];

john = null; // tulis ulang referensinya


// the object previously referenced by john is stored inside the array
// therefore it won't be garbage-collected
// we can get it as array[0]

Sama seperti itu, jika kita bisa menggunakan sebuah objek sebagai sebuah kunci/key didalam Map biasa, lalu selama Mapnya ada, objeknya akan selalu ada juga. Itu akan menempati memori dan mungkin tidak akan dibuang.

Contoh:

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // tulis ulang referensinya

// john disimpan didalam map,
// kita bisa mendapatkannya dengan menggunakan map.keys()

WeakMap secara dasar berbeda didalam aspek ini. Itu tidak akan mencegah pembuangan dari objek kunci.

Ayo kita lihat didalam contoh.

WeakMap

Perbedaan pertama dari Map adalah kunci WeakMap haruslah objek, bukan nilai primitif:

let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // bekerja dengan benar (object kunci/key)

// tidak bisa menggunakan string sebagai kunci
weakMap.set("test", "Whoops"); // Error, karena "test" bukanlah sebuah objek

Sekarang, jika kita menggunakan sebuah objek sebagai kunci didalamnya, dan disana tidak terdapat referensi lain ke objeknya – itu akan dihilangkan dari memori (dan juga dari map) secara otomatis.

let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // tulis ulang referensinya

// jogn telah dihilangkan dari memori!

Bandingkan itu dengan Map biasa dicontoh diatas. Sekarang jika john hanya ada jika sebagai kunci dari WeakMap – itu akan secara otomatis dihapus dari map (dan memori).

WeakMap tidak mendukung iterasi dan metode keys(), nilai(), entries(), jadi tidak ada cara untuk mendapatkan semua kunci atau nilai darinya.

WeakMap hanya mempunyai metode berikut:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

Kenapa terdapat batasan seperti itu? Itu hanyalah untuk alasan teknis. Jika sebuah objek kehilangan semua referensi lainnya (seperti john didalam kode diatas), lalu itu akan dibuang secara otomatis. Tapi secara teknis itu tidak benar-benar di spesifikasikan ketika pembersihan terjadi.

Mesin Javascript yang memilih hal itu. Itu mungkin akan memilih untuk melakukan pembersihan memori seketika atau menunggu dan melakukan pembersihan nanti ketika penghapusan lainnya terjadi. Jadi, secara teknis elemen count yang sekarang dari WeakMap tidak diketahui. Mesinnya mungkin sudah menghapusnya atau belum, atau sudah dihapus sebagian. Untuk alasan itu, metode yang mengakses seluruh key/nilai tidak didukung.

Sekarang dimana kita butuh struktur data seperti itu?

Kasus: tambahan data

Bagian utama dari penggunaan WeakMap adalah sebuah penambahan penyimpanan data.

Jika kita bekerja dengan sebuah objek yang “dimiliki” kode yang lain, bahkan mungkin sebuah librari pihak-ketiga, dan harus menyimpan beberapa data yang terkait dengannya, itu harus ada selama objeknya ada – lalu WeakMap adalah sesuatu yang dibutuhkan.

Kita menyimpan datanya kedalam WeakMap, menggunakan objek sebagai kunci, dan ketika objeknya dihapus, datanya akan secara otomatis menghilang juga.

weakMap.set(john, "secret documents");
// jika john meninggal, secret documents-nya akan dihapus secara otomatis

Kita lihat didalam contoh.

Contoh, kita mempunyai kode yang menyimpan hitungan kunjungan untuk pengguna. Informasinya disimpan didalam map: sebuah objek user adalah kunci dan hitungan kunjungan adalah nilainya. Ketika pengguna pergi (objeknya akan dihapus), kita tidak ingin kunjungan mereka dihitung lagi.

Ini adalah contoh dari fungsi penghitung dengan Map:

// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => kunjungan dihitung

// naikan hitungan kunjungan
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

Dan disini bagian kode lainnya, mungkin file lainnya akan menggunakannya:

// 📁 main.js
let john = { name: "John" };

countUser(john); // hitung kunjungannya

// lalu john pergi
john = null;

Sekarang objek john harusnya dihapus, tapi tetap berada di memori, itu sebagai kunci didalam visitsCountMap.

Kita perlu membersihkan visitsCountMap ketika kita menghapus pengguna, sebaliknya itu akan tetap didalam memori terus-menerus. Pembersihan seperti itu akan menjadi pekerjaan yang membosankan didalam arsitektur yang rumit.

Malahan kita bisa menghindarinya dengan berpindah ke WeakMap:

// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => kunjungan dihitung

// naikan hitungan kunjungan
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

Sekarang kita tidak harus membersihkan visitsCountMap. Setelah objek john menjadi tidak terjangkau lagi kecuali sebagai kunci dari WeakMap, itu akan dihilangkan dari memori, bersamaan dengan informasi kuncinya dari WeakMap.

Kasus: penyimpanan cache

Contoh biasa lainnya adalah penyimpanan cache: ketika sebuah hasil dari fungsi harus diingat (“di cache”), jadi didalam pemanggilan selanjutnya didalam objek yang sama bisa menggunakannya.

Kita bisa menggunakan Map untuk menyimpan hasil, seperti ini:

// 📁 cache.js
let cache = new Map();

// hitung dan ingat hasilnya
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* kalkulasi hasil */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// sekarang kita gunakan process() didalam file lainnya:

// 📁 main.js
let obj = {/* katakan kita mempunyai objek */};

let result1 = process(obj); // dihitung

// ...selanjutnya, dari bagian kode lainnya...
let result2 = process(obj); // ingat hasil yang diambil dari cache

// ...nanti, ketika objek tidak dibutuhkan lagi:
obj = null;

alert(cache.size); // 1 (Ouch! Objeknya masih didalam cache, menggunakan memori)

Untuk banyak pemanggilan dari process(obj) dengan objek yang sama, itu akan mengkalkulasikan hasilnya pertama kali, dan lalu hanya mengambilnya dari cache. kekurangannya adalah kita perlu membersihkan cache ketika objeknya tidak dibutuhkan lagi.

Jika kita mengganti Map dengan WeakMap, kemudian masalah ini menghilang: hasil yang di cache akan dihapus dari memori secara otomatis setelah objeknya dihapus.

// 📁 cache.js
let cache = new WeakMap();

// hitung dan ingat hasilnya
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* perhitungan hasil */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 📁 main.js
let obj = {/* objek */};

let result1 = process(obj);
let result2 = process(obj);

// ...nanti, ketika objeknya tidak dibutuhkan lagi:
obj = null;

// tidak bisa mendapatkan cache.size, karena itu WeakMap,
// tapi itu 0 atau nanti akan jadi 0
// Ketika obj dihapus, data cache akan dihapus juga

WeakSet

WeakSet memiliki perilaku yang sama:

  • Analoginya adalah untuk meng-Set, tapi mungkin kita hanya butuh menambahkan objek kedalam WeakSet (bukan primitif).
  • Sebuah objek ada didalam set selama itu bisa dijangkau dari tempat lain.
  • Seperti Set, itu mendukung add, has dan delete, tapi tidak size, keys() dan tidak ada iterasi

menjadi “weak”, itu juga menyediakan penyimpanan tambahan. Tapi tidak untuk data yang asal-asalan, tapi untuk “yes/no”. Keanggotaan dari WeakSet mungkin berarti sesuatu tentang objeknya.

Contoh, kita bisa menambahkan user kedalam WeakSet untuk mengetahui dari siapa saja yang mengunjungi website kita:

let visitedSet = new WeakSet();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

visitedSet.add(john); // John mengunjungi website
visitedSet.add(pete); // lalu Pete
visitedSet.add(john); // John lagi

// visitedSet sekarang memiliki 2 user

// periksa jika John telah berkunjung?
alert(visitedSet.has(john)); // true

// periksa jika Mary telah berkunjung?
alert(visitedSet.has(mary)); // false

john = null;

// visitedSet akan dibersihkan secara otomatis

Hal yang paling bisa diingat adalah batasan dari WeakMap dan WeakSet adalah tidak adanya iterasi, dan ketidak mampuan untuk mendapatkan seluruh konten saat ini. Itu mungkin akan merepotkan, tapi tidak mencegah WeakMap/WeakSet untuk melakukan tugas utama mereka – menjadi “tambahan” penyimpanan data dari objek yang disimpan/dikelola di tempat lain.

Ringkasan

WeakMap adalah koleksi seperti-Map yang mengijinkan hanya objek sebagai kunci dan menghapus mereka bersama dengan nilai yang terkait sekalinya mereka menjadi tidak terjangkau.

WeakSet adalah koleksi seperti-Set yang hanya menyimpan objek dan menghapus mereka sekalinya mereka menjadi tidak bisa diakses.

Keduanya tidak mendukung metode dan properti yang mengacu pada seluruh kunci atau jumlah mereka. Hanya operasi individual yang diperbolehkan.

WeakMap dan WeakSet digunakan sebagai struktur data “kedua” sebagai tambahan kepada penyimpanan objek “utama”. Sekalinya objeknya dihapus dari penyimpanan utama, jika itu hanya ditemukan sebagai kunci dari WeakMap atau didalam WeakSet, itu akan dihapus secara otomatis.

Tugas

Terdapat beberapa pesan dari array"

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

Kode kamu bisa mengaksesnya, tapi pesannya di kelola oleh kode orang lain. Pesan baru ditambahkan, pesan lama dihilangkan secara secara teratur oleh kode itu, dan kamu tidak tahu persis saat ketika itu terjadi.

Sekarang, struktur data mana yang harus kamu gunakan untuk menyimpan informasi tentang pesannya apakah “telah dibaca”? Strukturnya haruslah tepat untuk memberikan jawaban “apakah telah dibaca”? untuk pesan objek yang diberikan.

Catatan. Ketika sebuah pesan dihilangkan dari messages, pesan itu harus menghilang dari strukturnya juga.

Catatan tambahan. Kita seharusnya tidak memodifikasi objek message, tambahkan properti kita kedalamnya. Seperti mereka di kelola oleh kode orang lain, itu mungkin akan mengarah ke hasil akhir yang tidak diinginkan.

Ayo kita simpan pesan yang dibaca didalam WeakSet:

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

let readMessages = new WeakSet();

// dua pesan telah dibaca
readMessages.add(messages[0]);
readMessages.add(messages[1]);
// readMessages mempunyai 2 elemen

// ...sekarang baca pesan pertama lagi!
readMessages.add(messages[0]);
// readMessages masih memiliki 2 elemen yang unik

// jawaban: apakah message[0] telah dibaca?
alert("Read message 0: " + readMessages.has(messages[0])); // true

messages.shift();
// sekarang readMessages mempunyai 1 elemen (secara teknis memory mungkin akan dibersihkan nanti)

WeakSet membolehkan untuk menyimpan satu set dari messages dan dengan mudah memeriksa apakah sebuah pesan ada didalamnya.

Itu akan membersihkan dirinya sendiri secara otomatis. Timbal baliknya adalah kita tidak bisa melakukan iterasi didalamnya, tidak bisa mendapatkan “semua pesan yang telah dibaca” darinya secara langsung. Tapi kita bisa melakukan iterasi kepada seluruh pesan dan memfilter semuanya yang ada didalam set.

Hal lainnya, solusi berbeda bisa saja seperti menambahkan properti seperti message.isRead=true kepada pesan setelah pesannya dibaca. Seperti objek pesan dikelola oleh kode lain, hal itu tidak direkomendasikan, tapi kita bisa menggunakan properti simbol untuk menghindari konflik.

Seperti ini:

// properti simbol yang hanya diketahui kode kita
let isRead = Symbol("isRead");
messages[0][isRead] = true;

Sekarang kode dari pihak-ketiga kemungkinan tidak akan melihat properti tambahan kita.

walaupun simbol membolehkan kita untuk mengecilkan kemunculan dari masalah, menggunakan WeakSet lebih baik dari sisi arsitektural.

Terdapat sebuah array dari pesan sama seperti di previous task.Situasinya sama.

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

Pertanyaannya: struktur data mana yang kamu gunakan untuk menyimpan informasinya: “ketika pesannya dibaca?”.

Di tugas sebelumnya kita hanya menyimpan “yes/no”. Sekarang kita butuh untuk menyimpan tanggal, dan itu harus tetap berada di memori simpan sampai pesannya dibuang.

Catatan. Tanggal bisa disimpan sebagai objek dengan kelas bawaan `Date, kita akan mempelajarinya nanti.

Untuk menyimpan tanggal, kita bisa menggunakan WeakMap:

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

let readMap = new WeakMap();

readMap.set(messages[0], new Date(2017, 1, 1));
// Kita akan belajar objek Date nanti
Peta tutorial