Pada contoh-contoh kita selama ini, semua properti instance terbuka untuk diubah siapa pun. Kesalahan nyatanya pernah menimpa kafe Budi: satu modul lupa dengan aturan poin, menulis kartuSiti.poin = -999, dan laporan bulanan langsung tidak masuk akal. Data yang semestinya hanya bergerak lewat aturan class ternyata bisa digeser dari luar tanpa penjaga apa pun.

JavaScript modern menutup celah ini dengan private field: properti yang diawali tanda # dan hanya bisa dibaca maupun ditulis dari dalam badan class-nya sendiri. Chapter ini merakit RekeningPoin — penyimpan poin member dengan aturan tukar yang ketat — lalu membuktikan berlapis-lapis perlindungannya, termasuk galat yang muncul ketika luar mencoba menyusup.

#field: Milik Class, Bukan Milik Dunia Luar #

Aturannya tiga baris: field yang diawali # harus dideklarasikan di badan class; hanya kode di dalam badan class itu yang boleh membaca dan menulisnya; dan ia bukan properti biasa — rekeningBudi["#saldo"] tidak akan menemukannya, begitu pula Object.keys dan kawan-kawannya.

Mekanisme di balik layar beda jauh dari properti biasa. Field #saldo tidak disimpan dalam bentuk nama yang bisa dicari; engine memasangnya pada instance dengan identitas yang diketahui hanya oleh badan class. Kedua field tetap dihitung per instance — rekening Budi dan rekening Siti masing-masing membawa #saldo sendiri — yang disembunyikan adalah jalur aksesnya, bukan pemisahannya. Itulah sebabnya pelindungnya menembus dua fase sekaligus — parser menolak akses #saldo dari luar class, dan runtime menolak method yang dipanggil pada object yang bukan instance. Kita membuktikan keduanya di bagian berikut.

Rekening Poin dengan Aturan Ketat #

Berkas berikut memakai satu private field untuk data, satu private method untuk aturan internal, dan satu getter publik sebagai jendela baca:

// rekening-poin.js
// #field dan #method: internal class yang tidak bisa disentuh dari luar.

class RekeningPoin {
  #pemilik;
  #saldo;

  constructor(pemilik, saldoAwal) {
    this.#pemilik = pemilik;
    this.#saldo = saldoAwal;
  }

  #cekKecukupan(jumlah) {
    if (jumlah > this.#saldo) {
      throw new RangeError(`Saldo tidak cukup: minta ${jumlah}, tersedia ${this.#saldo}`);
    }
  }

  tambah(jumlah) {
    if (jumlah <= 0) {
      throw new RangeError(`Jumlah harus positif, diterima ${jumlah}`);
    }
    this.#saldo += jumlah;
  }

  tukar(jumlah) {
    this.#cekKecukupan(jumlah);
    this.#saldo -= jumlah;
    return this.#saldo;
  }

  get saldo() {
    return this.#saldo;
  }
}

const rekeningBudi = new RekeningPoin("Budi", 50);

rekeningBudi.tambah(30);
console.log(rekeningBudi.saldo);
console.log(rekeningBudi.tukar(60));

try {
  rekeningBudi.tukar(100);
} catch (err) {
  console.log(`${err.name}: ${err.message}`);
}

try {
  rekeningBudi.tambah.call({}, 10);
} catch (err) {
  console.log(`${err.name}: ${err.message}`);
}

Output yang Dihasilkan:

text
80
20
RangeError: Saldo tidak cukup: minta 100, tersedia 20
TypeError: Cannot read private member #saldo from an object whose class did not declare it

Mari kita bedah:

  1. Baris 5–6 (#pemilik; #saldo;): deklarasi dua private field. Keduanya lahir tanpa nilai, lalu diisi constructor.
  2. Baris 9–10 (isi di constructor): this.#saldo = saldoAwal sah karena berada di dalam badan class — inilah satu-satunya pintu awal untuk mengisi saldo.
  3. Baris 13–17 (#cekKecukupan): private method berawalan #. Ia dipanggil dari dalam class saja dan melempar RangeError bila permintaan tukar melebihi saldo.
  4. Baris 19–24 (tambah): method publik dengan penjagaan di gerbang — jumlah nol atau negatif ditolak sebelum #saldo digeser.
  5. Baris 26–30 (tukar): satu baris this.#cekKecukupan(jumlah) memanggil aturan internal, lalu saldo dipotong hanya bila lolos.
  6. Baris 32–34 (get saldo): getter publik membaca #saldo tanpa memberi jalur penulisan — jendela yang bisa dilihat, tidak bisa dibobol.
  7. Baris 40–41 (pemakaian normal): saldo Budi bergerak 50 → 80 → 20 semuanya lewat method resmi.
  8. Baris 43–47 (tukar melebihi saldo): RangeError tertangkap try...catch; saldo tetap 20, aturan class tidak terlanggar.
  9. Baris 49–53 (tambah.call({}, 10)): method publik dicoba dijalankan dengan this berupa object kosong buatan siapa saja — runtime menolaknya dengan TypeError karena object itu tidak punya field #saldo.

Akses dari Luar: Ditutup di Dua Lapis #

Pelindung #field bekerja pada dua fase yang berbeda, dan lapis pertama bahkan sebelum program berjalan. Perhatikan cuplikan berikut — object sudah dibuat, tetapi aksesnya ditulis di luar badan class:

js
// Demonstrasi: membaca #saldo dari luar badan class.
class RekeningPoin {
  #saldo = 0;
}

const rekeningBudi = new RekeningPoin();

console.log(rekeningBudi.#saldo);

Sintaks rekeningBudi.#saldo hanya boleh muncul di dalam class yang mendeklarasikan #saldo tersebut. Parser menolak berkas ini sejak awal; jika dijalankan, Node menampilkan transkrip seperti ini (path dipendekkan):

text
/home/…/akses-luar.js:8
console.log(rekeningBudi.#saldo);
                        ^

SyntaxError: Private field '#saldo' must be declared in an enclosing class

Pesan itu dapat dibaca langsung: field #saldo harus dideklarasikan di class yang mengurung baris ini. Tidak ada pengecualian, tidak ada celah lewat ["#saldo"] atau Object.keys — dari luar, field privat memang tidak ada.

Lapis kedua bekerja saat runtime dan kita sudah membuktikannya di baris 49–53 berkas utama. Method tambah adalah publik, sehingga bisa dipanggil dari mana saja; tetapi badannya membaca #saldo pada this. Ketika this diganti dengan object kosong lewat .call({}), runtime melempar TypeError: Cannot read private member #saldo from an object whose class did not declare it. Perlindungan ini berlaku bahkan pada object yang bentuknya mirip — yang dituntut bukan bentuk, melainkan field yang benar-benar dipasang constructor class ini.

Tak Terlihat dari Sisi Introspeksi #

Lapis pelindung #field juga lolos dari semua alat pengintip yang kita kenal di section Objects. Object.keys tidak mencantumkannya, Object.hasOwn tidak menemukannya, dan akses bracket dengan nama #saldo hanya menghasilkan undefined — bukan nilai, bukan pula galat:

js
class RekeningPoin {
  #saldo = 0;
  catatan = "tanpa poin";
}

const r = new RekeningPoin();
console.log(Object.keys(r));
console.log(Object.hasOwn(r, "catatan"));
console.log(Object.hasOwn(r, "#saldo"));
console.log(r["#saldo"]);

Output yang Dihasilkan:

text
[ 'catatan' ]
true
false
undefined

Hanya catatan — properti publik satu-satunya — yang muncul. Field privat sengaja dirancang tidak terhitung sebagai properti berobjek: ia terpasang pada instance dengan jalur internal yang tidak menampung nama. Konsekuensi praktisnya baik untuk kita pegang: data rahasia tidak bocor ke JSON.stringify, ke log Object.keys, maupun ke penelusuran for...in dari chapter collections.

Sementara itu, cara lama menyembunyikan data — menamai properti dengan garis bawah seperti _saldo — hanya kesepakatan tulisan. Kode lain masih bisa membaca dan menulisnya, dan tidak ada yang menghentikan. Perbedaannya penting dipegang:

PendekatanPelindungnyaBisa dilanggar?
_saldo (konvensi garis bawah)Kesepakatan tim, hanya sinyalYa — akses tetap berjalan normal
#saldo (private field)Aturan bahasa: parser dan runtimeTidak — ditolak dengan galat

#method dan Getter sebagai Jembatan #

Pasangan #cekKecukupan dan get saldo memperlihatkan pola kerja yang seimbang. Detail internal — cara menilai kecukupan saldo — disembunyikan sebagai #method, sehingga boleh diubah kapan saja tanpa mengejutkan pemakai. Yang perlu dijangkau luar disediakan sebagai jembatan publik yang sempit: tambah, tukar, dan saldo.

Jembatan baca seperti get saldo disebut getter dan hanya setengah ceritanya; JavaScript juga menyediakan set untuk jembatan tulis yang memvalidasi sebelum data disimpan. Keduanya adalah topik utuh chapter berikutnya.

Dengan internal yang terkunci, satu kebutuhan tersisa pada class kita: data dan helper yang miliknya class itu sendiri — bukan milik tiap instance — seperti penghitung jumlah kartu yang pernah dibuat. Kata kunci static yang membahasnya menunggu di chapter berikutnya.

Berkas Sumber

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

rekening-poin.js

Unduh berkas
javascript
// rekening-poin.js
// #field dan #method: internal class yang tidak bisa disentuh dari luar.

class RekeningPoin {
  #pemilik;
  #saldo;

  constructor(pemilik, saldoAwal) {
    this.#pemilik = pemilik;
    this.#saldo = saldoAwal;
  }

  #cekKecukupan(jumlah) {
    if (jumlah > this.#saldo) {
      throw new RangeError(`Saldo tidak cukup: minta ${jumlah}, tersedia ${this.#saldo}`);
    }
  }

  tambah(jumlah) {
    if (jumlah <= 0) {
      throw new RangeError(`Jumlah harus positif, diterima ${jumlah}`);
    }
    this.#saldo += jumlah;
  }

  tukar(jumlah) {
    this.#cekKecukupan(jumlah);
    this.#saldo -= jumlah;
    return this.#saldo;
  }

  get saldo() {
    return this.#saldo;
  }
}

const rekeningBudi = new RekeningPoin("Budi", 50);

rekeningBudi.tambah(30);
console.log(rekeningBudi.saldo);
console.log(rekeningBudi.tukar(60));

try {
  rekeningBudi.tukar(100);
} catch (err) {
  console.log(`${err.name}: ${err.message}`);
}

try {
  rekeningBudi.tambah.call({}, 10);
} catch (err) {
  console.log(`${err.name}: ${err.message}`);
}