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
- writable: false, nilai tidak bisa diganti. Di mode biasa, penggantian diabaikan diam-diam; di strict mode (termasuk di dalam
classdan ES module), munculTypeError. - enumerable: false, properti tidak muncul di
for...in,Object.keys(),JSON.stringify(), dan spread. Tetapi tetap bisa diakses langsung. - configurable: false, properti tidak bisa dihapus dengan
deletedan descriptor-nya tidak bisa diubah lagi (kecuali mengubahwritabledaritruekefalse).
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
- Lupa bahwa
definePropertydefault-nyafalse. Properti jadi tidak bisa diubah dan tidak muncul diObject.keys. Sebutkan flag secara eksplisit. - Setter memanggil dirinya sendiri.
set saldo(v) { this.saldo = v; }membuat rekursi tanpa akhir (Maximum call stack size exceeded). Simpan ke properti lain, misalnya_saldoatau private field#saldo(dibahas di modul Class). - Getter tanpa setter lalu diisi. Di strict mode memunculkan
TypeError, di mode biasa diabaikan. Tambahkan setter jika memang perlu ditulis. - Mengira
Object.freezeitu dalam. Object bersarang tetap bisa diubah; bekukan rekursif bila perlu. - Mencampur
value/writabledenganget/setdalam satu descriptor. Ini memunculkanTypeError: Invalid property descriptor. Pilih salah satu jenis.
Latihan
- Mudah, Buat object
aplikasidengan propertiversi: "1.0.0"yang tidak bisa diubah dan tidak muncul diObject.keys, memakaiObject.defineProperty. - Sedang, Buat object
suhudengan properti_celsiusdan pasangan getter/setterfahrenheit. Membacafahrenheitmengonversi dari Celsius; mengisinya menyimpan dalam Celsius. - 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
- Setiap properti punya descriptor:
value,writable,enumerable,configurable(atauget/set). - Properti biasa ber-flag
true; viaObject.definePropertydefault-nyafalse. enumerable: falsemenyembunyikan properti dari loop,Object.keys, danJSON.stringify.- Getter/setter membuat properti yang menghitung nilai atau memvalidasi input.
preventExtensions<seal<freezedalam tingkat penguncian, dan semuanya dangkal.