Belajar dengan membaca secara berurutan

Private Field (#) dan Enkapsulasi

Private field dan private method memakai awalan # untuk mencegah pembacaan dan penulisan dari luar class. Membahas SyntaxError sebelum kode jalan dan bedanya # dengan konvensi _.

Menyimpan saldo kartu prabayar di _balance tidak melindunginya: cukup tulis card._balance = 999999 di kode halaman mana pun, value itu langsung masuk. Awalan _ hanyalah konvensi yang berarti "jangan diubah dari luar"; konvensi ini tidak mencegah assignment maupun pemanggilan.

Artikel ini membahas private field dan private method, yang tidak bisa dibaca atau ditulis dari luar class.

Menyembunyikan Saldo di Dalam Class — Private Field

Misalnya saldo hanya boleh berubah lewat charge, yang mengecek batas top up 20000 yen per transaksi. Dengan _balance, kalau kode lain yang membuat layar refund menulis card._balance += 30000, tidak ada yang menghentikannya, dan nominal yang melebihi batas masuk ke saldo.

Private field (field di dalam class yang namanya diawali #, dan hanya bisa dibaca dan ditulis dari dalam kurung kurawal class itu) dideklarasikan sebagai #balance = 0; lalu dipakai dengan menulis this.#balance.

# juga bagian dari nama, jadi this.balance dan this.#balance adalah dua nama yang berbeda.

class PrepaidCard {
  #balance = 0;                              // Saldo. Tidak bisa dibaca atau ditulis dari luar class

  charge(amount) {
    if (amount > 20000) {
      throw new Error(`Top up maksimal 20000 yen per transaksi: ${amount}`);
    }
    this.#balance += amount;                 // Di dalam class, jadi bisa diubah
  }

  get balance() { return this.#balance; }    // Ke luar, hanya getter untuk membaca yang dibuka
}

const card = new PrepaidCard();
card.charge(5000);                           // Satu-satunya pintu untuk mengubah saldo adalah charge
card.charge(3000);
console.log(card.balance);                   // 8000
Baris yang Bisa dan Tak Bisa Akses #balance
Kurung kurawal class PrepaidCard
  • #balance = 0 — nama yang hanya bisa dipakai di dalam kurung kurawal ini
Body charge(amount)
  • Kalau amount > 20000, berhenti dengan throw
  • Hanya nominal yang lolos yang menjalankan this.#balance += amount
Body get balance()
  • return this.#balance — hanya mengembalikan value, tidak mengubahnya
Baris di luar class
  • card.charge(5000) — mengubah saldo lewat charge
  • card.balance — membaca 8000 lewat getter
  • Nama #balance tidak bisa ditulis di sini
#balance hanya bisa ditulis di dalam kurung kurawal class. Bagi baris di luar, satu-satunya pintu untuk mengubah saldo adalah charge.

Dengan enkapsulasi (desain yang menyatukan data dan kode yang mengubahnya di dalam class, lalu membatasi pemakaian dari luar hanya lewat pintu yang sudah ditentukan), satu-satunya cara mengubah saldo PrepaidCard ini adalah charge, dan satu-satunya cara membacanya adalah getter balance.

Nama # yang Belum Dideklarasikan Melempar SyntaxError

Deklarasikan nama # di body class, seperti #balance = 0; atau #balance;, sebelum memakainya. Kalau kamu meng-assign this.#balance = 0; di constructor tanpa deklarasi, hasilnya SyntaxError, walaupun barisnya ada di dalam class. Nama # tidak dibuat hanya dengan assignment.

Buat saldo poin member hanya bisa berubah lewat proses pembelian dan penukaran poin. PointAccount dan orders sudah dideklarasikan.

① Ubah saldo menjadi field yang tidak bisa dibaca atau ditulis dari luar class.

② Tambahkan points, yang hanya membaca saldo.

③ Proses pesanan satu per satu, dan tampilkan saldonya setiap kali.

④ Assign 999 ke nama lamanya, _points, lalu tampilkan "Saldo: X pt".

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Ditulis dari Luar, Berhenti Sebelum Kode Jalan — SyntaxError

Katakanlah kamu ingin mengatur saldo menjadi 999999 dari kode halaman untuk menguji sesuatu. Kalau kamu menulis card.#balance = 999999, seperti yang biasa kamu lakukan dengan _balance, baris itu gagal. Bahkan console.log di atasnya, yang seharusnya tampil lebih dulu, tidak menampilkan apa pun.

Menulis card.#balance di luar class melempar SyntaxError: Private field '#balance' must be declared in an enclosing class saat kode sedang dimuat, sama seperti mendeklarasikan ulang let. Pesan itu artinya "private field harus dideklarasikan di class yang membungkusnya".

class PrepaidCard {
  #balance = 0;

  charge(amount) { this.#balance += amount; }
  get balance() { return this.#balance; }
}

console.log("Mulai pengujian");   // Baris ini juga tidak ditampilkan

const card = new PrepaidCard();
card.charge(5000);

// Dengan _balance, baris dijalankan dari atas, tapi nama # menghentikan kode saat sedang dimuat
card.#balance = 999999;             // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log(card.balance);
Sejauh Mana Assignment dari Luar Berjalan
card._balance= 999999Jalan mulai daribaris pertama"Mulai pengujian"ditampilkanAssignment masuk:saldo 999999card.#balance= 999999Sintaks diceksaat dimuatAda nama # diluar classError ketahuansebelum jalan
Baris atas berjalan sampai assignment, dan output sebelumnya tetap muncul. Baris bawah berhenti sebelum baris pertama dijalankan.

Kalau kamu mendapat SyntaxError yang diawali Private field tanpa ada output sama sekali, cari nama # yang ditulis di luar class. Ubah baris yang mengganti value dari luar menjadi pemanggilan method class, seperti charge.

Halaman toko menambah sisa pemakaian kupon kunjungan dengan menulis langsung ke nama #. Ubah bagian itu menjadi pemanggilan method class. VisitCoupon dan coupon sudah dideklarasikan.

① Pakai kupon 2 kali, lalu tampilkan "Sisa X kali".

② Tambahkan addUses ke class supaya sisa pemakaiannya tidak pernah lebih dari 5.

③ Tambahkan 2 kali, lalu tampilkan sisa pemakaiannya.

④ Tambahkan 4 kali lagi, lalu tampilkan sisa pemakaiannya.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Menyembunyikan Langkah Internal dari Luar — Private Method

Misalnya kode yang mengurangi saldo kamu pindahkan dari pay ke method biasa bernama withdraw. Sekarang kode di luar class juga bisa memanggil card.withdraw(5000), melewati pengecekan saldo di pay, dan membuat saldonya minus.

Kalau kamu menjadikannya private method (method yang didefinisikan dengan awalan # dan hanya bisa dipanggil dari method di class yang sama dalam bentuk this.#withdraw(price)), langkah yang kamu pindahkan itu tidak bisa dipanggil dari luar. Kalau nanti nama atau parameternya diubah, bagian yang perlu diperbaiki tetap berada di dalam class.

class PrepaidCard {
  #balance = 3000;

  // Langkah yang hanya mengurangi saldo. Cukup atau tidaknya dicek dulu oleh pay
  #withdraw(amount) { this.#balance -= amount; }

  pay(price) {
    if (price > this.#balance) {
      return `Saldo tidak cukup: ${price}`;
    }
    this.#withdraw(price);                     // Di dalam class, jadi bisa dipanggil
    return `Saldo setelah pembayaran: ${this.#balance}`;
  }
}

const card = new PrepaidCard();
console.log(card.pay(480));                    // Saldo setelah pembayaran: 2520
console.log(card.pay(5000));                   // Saldo tidak cukup: 5000
console.log(typeof card.withdraw);             // undefined (kalau dipanggil, TypeError)
Pemanggilan yang Sampai ke #withdraw
#withdraw(amount)hanya memotongsaldocard.pay(480)card.pay(5000)card.withdraw(5000)480 tidak melebihisaldo 30005000 melebihisaldo 2520Tidak ada methodwithdraw (tanpa #)Lewat #withdraw:2520Tidak sampai ke#withdraw: 2520Berhenti denganTypeError
Hanya kolom kiri, yang lolos pengecekan, yang sampai ke langkah pengurangan saldo. Nama withdraw tanpa # tidak ada di instance.

pay(5000) langsung return di pengecekan saldo, jadi tidak pernah sampai ke #withdraw. Menulis card.#withdraw(5000) di luar class melempar SyntaxError saat kode dimuat, sama seperti #balance; TypeError di kolom kanan berasal dari pemanggilan nama lain, yaitu nama tanpa #.

Panggil Private Method dengan this.

Di dalam class pun, kalau kamu menghilangkan this. dan menulis #withdraw(price);, kodenya berhenti dengan SyntaxError: Unexpected identifier '#withdraw' sebelum apa pun dijalankan. Sama seperti private field, private method selalu dipanggil dalam bentuk this.#withdraw(price).

Kamu menghitung biaya sewa sepeda saat ini dan biaya tambahan untuk perpanjangan dengan rumus yang sama. BikeRental dan rides sudah dideklarasikan.

① Tambahkan calcFee, yang menghitung biaya, sebagai method yang tidak bisa dipanggil dari luar.

② Dengan memakai ①, tambahkan property fee yang mengembalikan biaya saat ini dan method extraFee yang mengembalikan biaya tambahan untuk perpanjangan.

③ Tampilkan "X menit: X yen (+X yen untuk tambahan 10 menit)" untuk semua penyewaan.

④ Baca calcFee pada penyewaan pertama dari luar, lalu tampilkan tipenya.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Apa yang Tersimpan di JSON — Beda _ dan #

Sekarang class kartu prabayar kamu ubah dari _balance menjadi #balance. Assignment dari luar memang jadi tercegah, tapi kalau selama ini instance disimpan sebagai string dengan JSON.stringify, saldo tidak lagi ada di data yang disimpan setelah perubahan itu.

Nama berawalan _ hanyalah property biasa yang kebetulan diawali _, jadi muncul di JSON.stringify. Field # tidak termasuk key property, jadi tidak ikut di hasilnya. Getter didefinisikan di class, bukan di instance, jadi juga bukan key, dan balance pun tidak muncul.

// Sebelum diubah: property biasa dengan awalan _
class LegacyPrepaidCard {
  constructor() { this._balance = 3000; }
}

// Setelah diubah: private field dengan # dan getter untuk membacanya
class PrepaidCard {
  #balance = 3000;
  get balance() { return this.#balance; }
}

const legacyCard = new LegacyPrepaidCard();
const prepaidCard = new PrepaidCard();

console.log(JSON.stringify(legacyCard));                         // {"_balance":3000}
console.log(JSON.stringify(prepaidCard));                        // {}
console.log(JSON.stringify({ balance: prepaidCard.balance }));   // {"balance":3000}
Yang Dibaca JSON.stringify
prepaidCard (instance yang punya #balance)
Key property
  • Tidak ada key sama sekali (get balance() adalah method yang ditulis di class)
  • JSON.stringify(prepaidCard) menghasilkan {}
Private field
  • #balance — 3000
  • Hanya baris di dalam PrepaidCard yang bisa membacanya
#balance ada di instance, tapi bukan key property. JSON.stringify hanya membaca key property.

Walaupun hasilnya {}, saldonya tidak hilang: prepaidCard.balance tetap membaca 3000. Saat menyimpan, oper object yang dibuat dari value getter, seperti di baris terakhir contoh. Tabel di bawah membandingkan tiga cara menulis yang dipakai di artikel ini.

Cara menulisAkses dari luar classHasil JSON.stringify
Nama dengan _ (_balance)Bisa dibaca dan ditulis, tanpa cekMuncul: {"_balance":3000}
Private field (#balance)SyntaxError saat dimuatTidak muncul ({})
Private method (#withdraw)#withdraw(): SyntaxError / withdraw(): TypeErrorTidak muncul (method)
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Apa yang terjadi saat kamu menjalankan kode yang berisi card.#balance = 0; di luar class, setelah sebuah console.log?

Soal 2Pada card yang punya #withdraw, apa yang terjadi kalau kamu memanggil card.withdraw(500) dari luar?

Soal 3Apa hasil JSON.stringify untuk card yang punya #balance = 3000 dan hanya sebuah getter?