kodingup.camp

Beranda/JavaScript Mastery: Object & Data

Mengelola Data Kunci-Nilai

Modul 14 dari 144 menit bacaKey-Value Data Management

Isi modul

    Kenapa ini penting

    Selama ini kamu memakai object biasa sebagai "kamus" untuk menyimpan pasangan kunci-nilai: hitungan kata, harga per produk, pengaturan. Itu bekerja, tapi object biasa punya keterbatasan: kuncinya harus string atau Symbol, ia mewarisi properti dari prototype (yang bisa bertabrakan dengan kunci seperti "constructor"), dan tidak ada cara langsung untuk mengetahui jumlah isinya.

    JavaScript menyediakan struktur data khusus untuk koleksi: Map (kamus dengan kunci tipe apa pun), Set (himpunan nilai unik), serta saudaranya WeakMap dan WeakSet yang dirancang supaya tidak menghalangi pembersihan memori. Memilih struktur yang tepat membuat kode lebih cepat, lebih aman, dan lebih jelas maksudnya. Ini adalah modul penutup materi Object & Data, sekaligus jembatan ke pola-pola lanjutan seperti caching dan pelacakan state.

    Konsep inti

    Map

    Map menyimpan pasangan kunci-nilai dengan beberapa keunggulan dibanding object biasa:

    Method utama: set(k, v) (mengembalikan Map itu sendiri sehingga bisa dirangkai), get(k), has(k), delete(k), clear(), serta keys(), values(), entries(), dan forEach. Konversi: new Map(Object.entries(obj)) dan Object.fromEntries(map).

    Analogi: object biasa seperti buku telepon yang hanya bisa diurutkan berdasarkan nama (string). Map seperti lemari penitipan yang kuncinya boleh berupa apa saja: kartu, gelang, atau token.

    Peringatan: jangan menulis map["kunci"] = nilai. Itu menambah properti biasa pada object Map, bukan entri Map, sehingga tidak terdeteksi oleh get, has, atau size.

    Set

    Set adalah koleksi nilai unik. Menambahkan nilai yang sudah ada tidak berpengaruh. Method: add(v), has(v), delete(v), clear(), size. has pada Set jauh lebih cepat daripada includes pada array besar, karena Set tidak perlu memeriksa elemen satu per satu.

    Keunikan dicek dengan algoritma SameValueZero: mirip ===, tapi NaN dianggap sama dengan NaN. Untuk object, yang dibandingkan referensinya.

    ES2025 menambahkan method himpunan langsung pada Set: union (gabungan), intersection (irisan), difference (selisih), symmetricDifference, isSubsetOf, isSupersetOf, dan isDisjointFrom. Tersedia di Node.js 22 ke atas dan browser modern.

    WeakMap dan WeakSet

    Di JavaScript, memori dibersihkan otomatis oleh garbage collector (pengumpul sampah) ketika sebuah object tidak lagi dirujuk oleh siapa pun. Masalahnya, kalau kamu menyimpan object sebagai kunci di Map, Map itu tetap merujuknya, sehingga object tidak pernah dibersihkan walaupun bagian lain programmu sudah tidak memakainya. Ini disebut memory leak (kebocoran memori).

    WeakMap dan WeakSet menyimpan rujukan lemah: kalau object kuncinya tidak dipakai di tempat lain, ia boleh dibersihkan dan entrinya ikut hilang. Konsekuensinya:

    Kegunaan umum: menyimpan data tambahan atau cache untuk object yang kamu tidak "miliki" (misalnya elemen DOM atau object dari library), dan menandai object yang sudah diproses.

    Kapan memakai Map vs object

    Situasi Pilihan
    Struktur tetap dengan field yang diketahui (data user, konfigurasi) object
    Data yang akan diubah ke JSON / dikirim ke API object
    Kunci berupa object, angka yang harus tetap angka, atau tipe campuran Map
    Sering menambah/menghapus entri, butuh size Map
    Kunci berasal dari input pengguna (risiko tabrakan prototype) Map
    Hanya perlu menyimpan nilai unik dan cek keberadaan Set
    Menyimpan data tambahan per object tanpa menghalangi garbage collection WeakMap / WeakSet

    Satu catatan: JSON.stringify(map) menghasilkan "{}". Ubah dulu dengan Object.fromEntries(map) atau [...map] sebelum diserialisasi.

    Contoh kode

    // 1) Map dasar
    const stok = new Map();
    stok.set("kopi", 12).set("teh", 0).set("coklat", 5); // set bisa dirangkai
    console.log(stok.get("kopi"), stok.has("teh"), stok.size); // → 12 true 3
    stok.delete("teh");
    console.log([...stok.keys()]);   // → [ 'kopi', 'coklat' ]
    console.log(stok.get("jus"));    // → undefined
    
    // 2) Kunci tipe apa pun
    const m = new Map();
    m.set(1, "angka satu");
    m.set("1", "teks satu");
    const userA = { nama: "Ana" };
    m.set(userA, "data milik Ana");
    console.log(m.get(1), "|", m.get("1"), "|", m.get(userA)); // → angka satu | teks satu | data milik Ana
    console.log(m.get({ nama: "Ana" })); // → undefined (object berbeda referensi)
    
    // 3) Iterasi dan konversi
    const harga = new Map(Object.entries({ kopi: 18000, teh: 12000 }));
    for (const [menu, rp] of harga) console.log(menu, rp);
    // → kopi 18000
    // → teh 12000
    console.log(Object.fromEntries(harga)); // → { kopi: 18000, teh: 12000 }
    console.log(JSON.stringify(harga));     // → {}  (Map tidak terserialisasi langsung)
    console.log(JSON.stringify(Object.fromEntries(harga))); // → {"kopi":18000,"teh":12000}
    
    // 4) Jebakan: kurung siku bukan entri Map
    const salah = new Map();
    salah["kunci"] = "nilai";
    console.log(salah.size, salah.has("kunci")); // → 0 false
    
    // 5) Map aman untuk kunci dari input pengguna
    const hitungObj = {};
    const hitungMap = new Map();
    for (const kata of ["constructor", "halo", "halo"]) {
      hitungMap.set(kata, (hitungMap.get(kata) ?? 0) + 1);
    }
    console.log(typeof hitungObj["constructor"]); // → function (warisan prototype, bisa merusak hitungan)
    console.log(hitungMap.get("constructor"), hitungMap.get("halo")); // → 1 2
    
    // 6) Set dasar
    const pengunjung = new Set();
    pengunjung.add("ana").add("budi").add("ana");
    console.log(pengunjung.size, pengunjung.has("budi")); // → 2 true
    console.log(new Set([NaN, NaN, 0, -0]).size);         // → 2 (NaN dianggap sama, 0 dan -0 juga)
    
    // 7) Operasi himpunan (ES2025, Node 22+)
    const kelasA = new Set(["Ani", "Budi", "Caca"]);
    const kelasB = new Set(["Budi", "Dodi"]);
    console.log([...kelasA.union(kelasB)]);        // → [ 'Ani', 'Budi', 'Caca', 'Dodi' ]
    console.log([...kelasA.intersection(kelasB)]); // → [ 'Budi' ]
    console.log([...kelasA.difference(kelasB)]);   // → [ 'Ani', 'Caca' ]
    console.log(new Set(["Budi"]).isSubsetOf(kelasA)); // → true
    
    // Cara klasik yang jalan di lingkungan lama
    const irisanKlasik = [...kelasA].filter((n) => kelasB.has(n));
    console.log(irisanKlasik); // → [ 'Budi' ]
    
    // 8) WeakMap: data privat / cache per object
    const dataPrivat = new WeakMap();
    class Akun {
      constructor(pemilik, pin) {
        this.pemilik = pemilik;
        dataPrivat.set(this, { pin }); // pin tidak terlihat sebagai properti
      }
      cekPin(pin) {
        return dataPrivat.get(this).pin === pin;
      }
    }
    const akun = new Akun("Dewi", "1234");
    console.log(akun.cekPin("1234"), Object.keys(akun)); // → true [ 'pemilik' ]
    
    try {
      dataPrivat.set("teks", 1); // kunci primitif tidak boleh
    } catch (err) {
      console.log(err.constructor.name); // → TypeError
    }
    console.log(dataPrivat.size); // → undefined (WeakMap tidak punya size)
    
    // Cache hasil perhitungan berat per object
    const cacheTotal = new WeakMap();
    function totalKeranjang(keranjang) {
      if (cacheTotal.has(keranjang)) return `${cacheTotal.get(keranjang)} (cache)`;
      const total = keranjang.items.reduce((s, i) => s + i.harga, 0);
      cacheTotal.set(keranjang, total);
      return `${total} (hitung)`;
    }
    const keranjangku = { items: [{ harga: 20000 }, { harga: 35000 }] };
    console.log(totalKeranjang(keranjangku)); // → 55000 (hitung)
    console.log(totalKeranjang(keranjangku)); // → 55000 (cache)
    
    // 9) WeakSet: menandai object yang sudah diproses
    const sudahDikirim = new WeakSet();
    function kirimEmail(user) {
      if (sudahDikirim.has(user)) return `Lewati ${user.email}`;
      sudahDikirim.add(user);
      return `Kirim ke ${user.email}`;
    }
    const u1 = { email: "a@mail.id" };
    console.log(kirimEmail(u1)); // → Kirim ke a@mail.id
    console.log(kirimEmail(u1)); // → Lewati a@mail.id
    

    Kesalahan umum

    1. Memakai map[kunci] = nilai alih-alih map.set(kunci, nilai). Itu membuat properti biasa yang tidak terdeteksi get, has, atau size.
    2. Mengharapkan get menemukan object dengan isi sama. Map membandingkan kunci object berdasarkan referensi. Simpan referensi object yang sama, atau pakai kunci primitif seperti id.
    3. Mengirim Map atau Set langsung ke JSON.stringify. Hasilnya "{}". Ubah dulu dengan Object.fromEntries(map) atau [...set].
    4. Mencoba me-loop atau membaca size WeakMap/WeakSet. Keduanya sengaja tidak iterable. Kalau butuh daftar isi, pakai Map/Set biasa.
    5. Memakai Map untuk segala hal. Untuk data berstruktur tetap yang dikirim sebagai JSON, object biasa lebih sederhana dan lebih umum dipakai.

    Latihan

    1. Mudah, Buat Set dari array ["merah", "biru", "merah", "hijau", "biru"]. Cetak jumlah warna unik dan cek apakah "kuning" ada.
    2. Sedang, Buat function hitungFrekuensi(arr) yang mengembalikan Map berisi jumlah kemunculan setiap elemen, lalu ambil elemen yang paling sering muncul.
    3. Menantang, Buat function memo(fn) yang mengembalikan versi function dengan cache memakai Map (kunci = argumen tunggal primitif). Hitung berapa kali function aslinya benar-benar dipanggil saat kuadrat dipanggil dengan argumen 4, 4, 5, 4.
    Lihat solusi
    // 1) Mudah
    const warna = new Set(["merah", "biru", "merah", "hijau", "biru"]);
    console.log(warna.size, warna.has("kuning")); // → 3 false
    
    // 2) Sedang
    function hitungFrekuensi(arr) {
      const frek = new Map();
      for (const item of arr) frek.set(item, (frek.get(item) ?? 0) + 1);
      return frek;
    }
    const frek = hitungFrekuensi(["a", "b", "a", "c", "a", "b"]);
    console.log(frek); // → Map(3) { 'a' => 3, 'b' => 2, 'c' => 1 }
    const [terbanyak] = [...frek].reduce((maks, entri) => (entri[1] > maks[1] ? entri : maks));
    console.log(terbanyak); // → a
    
    // 3) Menantang
    function memo(fn) {
      const cache = new Map();
      return (x) => {
        if (!cache.has(x)) cache.set(x, fn(x));
        return cache.get(x);
      };
    }
    let jumlahPanggilan = 0;
    const kuadrat = memo((n) => {
      jumlahPanggilan++;
      return n * n;
    });
    console.log([4, 4, 5, 4].map(kuadrat)); // → [ 16, 16, 25, 16 ]
    console.log(jumlahPanggilan);           // → 2
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.