kodingup.camp

Beranda/JavaScript Mastery: Prototype, Class, Waktu, Error & Async

Mengatur Perilaku Properti Object

Modul 2 dari 153 menit bacaObject Property Configuration

Isi modul

    Kenapa ini penting

    Selama ini kamu menganggap properti object hanyalah pasangan kunci dan nilai. Sebenarnya setiap properti punya "pengaturan tersembunyi": boleh diubah atau tidak, muncul saat di-loop atau tidak, boleh dihapus atau tidak. Bayangkan properti seperti loker di sekolah. Selain isinya, loker punya aturan: ada yang bisa dibuka siapa saja, ada yang dikunci, ada yang tidak tercantum di daftar loker.

    Pengetahuan ini penting karena banyak hal di JavaScript bekerja lewat mekanisme ini. Kenapa for...in tidak menampilkan method toString? Kenapa Math.PI tidak bisa diubah? Bagaimana library seperti Vue versi lama membuat data "reaktif"? Jawabannya: property descriptor serta getter dan setter. Dengan memahaminya, kamu bisa membuat object yang lebih aman (konstanta yang benar-benar tidak bisa diubah) dan lebih pintar (properti yang menghitung nilainya sendiri atau memvalidasi input).

    Konsep inti

    Property descriptor

    Setiap properti punya descriptor, yaitu object kecil yang menjelaskan sifatnya. Ada dua jenis:

    Data descriptor Accessor descriptor
    value: nilainya get: function saat dibaca
    writable: boleh diubah? set: function saat diisi
    enumerable: muncul saat di-loop? enumerable
    configurable: boleh dihapus/dikonfigurasi ulang? configurable

    Lihat descriptor dengan Object.getOwnPropertyDescriptor(obj, "kunci").

    Properti yang dibuat dengan cara biasa (obj.x = 1 atau literal { x: 1 }) punya ketiga flag bernilai true. Tetapi properti yang dibuat dengan Object.defineProperty punya flag default false jika tidak kamu sebutkan. Ini jebakan klasik.

    Arti tiap flag

    Getter dan setter

    Getter adalah function yang berjalan saat properti dibaca, setter berjalan saat properti diisi. Dari luar, keduanya terlihat seperti properti biasa. Analoginya seperti petugas loket: kamu bilang "saya mau saldo", petugas menghitung dulu lalu menjawab. Kamu tidak perlu tahu prosesnya.

    const obj = {
      get luas() { return this.p * this.l; },
      set sisi(v) { this.p = v; this.l = v; },
    };
    

    Kunci seluruh object

    Method Tambah properti Hapus Ubah nilai
    Object.preventExtensions Tidak Ya Ya
    Object.seal Tidak Tidak Ya
    Object.freeze Tidak Tidak Tidak

    Ingat, freeze juga dangkal: object di dalamnya masih bisa diubah.

    Contoh kode

    "use strict"; // supaya pelanggaran aturan memunculkan error, bukan diam-diam
    
    // 1. Melihat descriptor properti biasa
    const produk = { nama: "Laptop", harga: 9500000 };
    console.log(Object.getOwnPropertyDescriptor(produk, "nama"));
    // → { value: 'Laptop', writable: true, enumerable: true, configurable: true }
    
    // 2. defineProperty: flag default-nya false!
    Object.defineProperty(produk, "sku", { value: "LP-001" });
    console.log(Object.getOwnPropertyDescriptor(produk, "sku"));
    // → { value: 'LP-001', writable: false, enumerable: false, configurable: false }
    console.log(Object.keys(produk)); // → [ 'nama', 'harga' ]  (sku tersembunyi)
    console.log(produk.sku);          // → LP-001 (tetap bisa dibaca langsung)
    
    try {
      produk.sku = "LAIN";
    } catch (e) {
      console.log(e.constructor.name); // → TypeError
    }
    
    // 3. Properti internal yang tidak ikut ke JSON
    const sesi = { user: "sari" };
    Object.defineProperty(sesi, "_token", {
      value: "abc123",
      enumerable: false,
      writable: true,
    });
    console.log(JSON.stringify(sesi)); // → {"user":"sari"}
    
    // 4. Getter & setter dengan validasi
    const rekening = {
      _saldo: 0,
      get saldo() {
        return `Rp${this._saldo.toLocaleString("id-ID")}`;
      },
      set saldo(nilai) {
        if (typeof nilai !== "number" || nilai < 0) {
          throw new RangeError("Saldo harus angka >= 0");
        }
        this._saldo = nilai;
      },
    };
    rekening.saldo = 1500000;
    console.log(rekening.saldo); // → Rp1.500.000
    try {
      rekening.saldo = -5;
    } catch (e) {
      console.log(e.message); // → Saldo harus angka >= 0
    }
    
    // 5. Getter lewat defineProperty: properti turunan (computed)
    const orang = { depan: "Rina", belakang: "Wijaya" };
    Object.defineProperty(orang, "namaLengkap", {
      get() { return `${this.depan} ${this.belakang}`; },
      enumerable: true,
    });
    orang.depan = "Dewi";
    console.log(orang.namaLengkap); // → Dewi Wijaya (selalu terbaru)
    
    // 6. freeze untuk konfigurasi
    const CONFIG = Object.freeze({ API_URL: "https://api.contoh.id", TIMEOUT: 5000 });
    try {
      CONFIG.TIMEOUT = 1;
    } catch (e) {
      console.log("Tidak bisa diubah:", e.constructor.name); // → Tidak bisa diubah: TypeError
    }
    console.log(Object.isFrozen(CONFIG)); // → true
    
    // 7. defineProperties sekaligus banyak
    const lingkaran = {};
    Object.defineProperties(lingkaran, {
      r: { value: 7, writable: true, enumerable: true },
      luas: { get() { return +(Math.PI * this.r ** 2).toFixed(2); }, enumerable: true },
    });
    console.log(lingkaran.luas); // → 153.94
    

    Kode di atas memakai "use strict". Kalau kamu menjalankannya tanpa strict mode, baris produk.sku = "LAIN" tidak error, tetapi nilainya juga tidak berubah. Kegagalan diam-diam seperti ini lebih berbahaya daripada error, karena kamu tidak sadar ada yang salah.

    Kesalahan umum

    1. Lupa bahwa defineProperty default-nya false. Properti jadi tidak bisa diubah dan tidak muncul di Object.keys. Sebutkan flag secara eksplisit.
    2. Setter memanggil dirinya sendiri. set saldo(v) { this.saldo = v; } membuat rekursi tanpa akhir (Maximum call stack size exceeded). Simpan ke properti lain, misalnya _saldo atau private field #saldo (dibahas di modul Class).
    3. Getter tanpa setter lalu diisi. Di strict mode memunculkan TypeError, di mode biasa diabaikan. Tambahkan setter jika memang perlu ditulis.
    4. Mengira Object.freeze itu dalam. Object bersarang tetap bisa diubah; bekukan rekursif bila perlu.
    5. Mencampur value/writable dengan get/set dalam satu descriptor. Ini memunculkan TypeError: Invalid property descriptor. Pilih salah satu jenis.

    Latihan

    1. Mudah, Buat object aplikasi dengan properti versi: "1.0.0" yang tidak bisa diubah dan tidak muncul di Object.keys, memakai Object.defineProperty.
    2. Sedang, Buat object suhu dengan properti _celsius dan pasangan getter/setter fahrenheit. Membaca fahrenheit mengonversi dari Celsius; mengisinya menyimpan dalam Celsius.
    3. Menantang, Tulis function deepFreeze(obj) yang membekukan object beserta semua object di dalamnya secara rekursif.
    Lihat solusi
    // 1
    const aplikasi = { nama: "KasirKu" };
    Object.defineProperty(aplikasi, "versi", { value: "1.0.0" }); // flag default false
    console.log(Object.keys(aplikasi), aplikasi.versi); // → [ 'nama' ] 1.0.0
    
    // 2
    const suhu = {
      _celsius: 0,
      get fahrenheit() { return this._celsius * 9 / 5 + 32; },
      set fahrenheit(f) { this._celsius = (f - 32) * 5 / 9; },
    };
    suhu.fahrenheit = 212;
    console.log(suhu._celsius, suhu.fahrenheit); // → 100 212
    
    // 3
    function deepFreeze(obj) {
      for (const nilai of Object.values(obj)) {
        if (nilai && typeof nilai === "object") deepFreeze(nilai);
      }
      return Object.freeze(obj);
    }
    const cfg = deepFreeze({ db: { host: "localhost" } });
    cfg.db.host = "x"; // diabaikan (non-strict) atau TypeError (strict)
    console.log(cfg.db.host); // → localhost
    

    Ringkasan

    Kuis pemahaman

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