Manajemen kafe Budi meminta satu angka sederhana: berapa kartu anggota yang sudah dibuat program sejauh ini. Penampung pertama yang terpikir adalah variabel di luar class — tetapi variabel itu punya umur sendiri, bisa diubah siapa saja, dan terpisah dari class yang seharusnya menjadi pemiliknya. Menyalin penghitung ke tiap instance juga salah, karena nilainya justru harus sama untuk semua kartu.

Data dan helper semacam ini memang bukan milik instance — miliknya class itu sendiri. Kata kunci static menyediakan tempat resminya. Chapter ini menandai anggota static di class KartuAnggota, membuktikan instance tidak mewarisinya, dan menutup dengan pola populer yang memanfaatkannya: static factory.

static: Menempel di Class Itu Sendiri #

Aturannya: anggota yang dideklarasikan dengan static di badan class menjadi properti atau method dari object class — bukan dari instance hasil new. Konsekuensinya mengikuti aturan akses yang sudah kita kenal: dipanggil lewat nama class (KartuAnggota.totalDibuat), dan instance tidak melihatnya karena static tidak ikut masuk ke prototype yang dibagi ke instance.

Pembedaan ini penting untuk dipahami dengan kerangka chapter-chapter sebelumnya. Method biasa disimpan di KartuAnggota.prototype sehingga terjangkau dari setiap instance lewat rantai prototype; anggota static disimpan langsung pada object class KartuAnggota — satu tingkat di luar jangkauan instance.

Penghitung dan Factory dalam Satu Berkas #

Berkas berikut melengkapi KartuAnggota dengan dua static field dan satu static factory:

// kartu-anggota.js
// static: anggota yang menempel di class, bukan di tiap instance.

class KartuAnggota {
  static totalDibuat = 0;
  static bonusPendaftaran = 20;

  constructor(nama, poin) {
    this.nama = nama;
    this.poin = poin + KartuAnggota.bonusPendaftaran;
    KartuAnggota.totalDibuat += 1;
  }

  static dariPendaftaran(nama, teksPoin) {
    const poin = Number(teksPoin) || 0;
    return new KartuAnggota(nama, poin);
  }
}

const kartuBudi = new KartuAnggota("Budi", 100);
const kartuSiti = KartuAnggota.dariPendaftaran("Siti", "85");

console.log(kartuBudi.poin);
console.log(kartuSiti.poin);
console.log(KartuAnggota.totalDibuat);

console.log(kartuBudi.dariPendaftaran);
console.log(kartuBudi.bonusPendaftaran);
console.log(KartuAnggota.bonusPendaftaran);

console.log(Object.hasOwn(KartuAnggota, "dariPendaftaran"));
console.log(Object.hasOwn(kartuBudi, "dariPendaftaran"));

Output yang Dihasilkan:

text
120
105
2
undefined
undefined
20
true
false

Mari kita bedah:

  1. Baris 5–6 (static totalDibuat = 0; static bonusPendaftaran = 20;): dua static field — satu penghitung yang hidup di class, satu konstanta aturan bonus yang juga milik class.
  2. Baris 10 (this.poin = poin + KartuAnggota.bonusPendaftaran;): constructor membaca static field lewat nama class — cara resmi mengakses anggota static dari dalam.
  3. Baris 11 (KartuAnggota.totalDibuat += 1;): setiap new menambah penghitung yang sama; tidak ada salinan per instance.
  4. Baris 14–17 (static dariPendaftaran): static factory — method di tingkat class yang menerima data mentah dari input, membersihkannya, lalu mengembalikan instance baru dengan new.
  5. Baris 21 (KartuAnggota.dariPendaftaran("Siti", "85")): teks "85" dari form diterima apa adanya; factory mengonversinya menjadi angka 85, lalu bonus pendaftaran mengangkat poin Siti menjadi 105.
  6. Baris 25 (KartuAnggota.totalDibuat): angka 2 tercetak — dua jalur pembuatan kartu, satu penghitung yang sama.
  7. Baris 27–28 (kartuBudi.dariPendaftaran dan kartuBudi.bonusPendaftaran): keduanya undefined di instance — bukti anggota static tidak ditemukan lewat rantai prototype instance.
  8. Baris 31–32 (Object.hasOwn): alat pemeriksaan kunci sendiri dari chapter prototype chain mengonfirmasi posisinya — dariPendaftaran adalah properti sendiri dari object class, bukan dari kartuBudi.

Instance Tidak Mewarisi static #

Dua undefined di berkas tadi adalah kesimpulan utama bagian ini: pencarian properti dari instance menyusuri [[Prototype]] instance, dan rantai itu tidak pernah melewati object class. Field bonusPendaftaran menempel di KartuAnggota, bukan di KartuAnggota.prototype tempat method biasa bersemayam, sehingga instance tidak punya jalur ke sana.

Tetap ada satu jembatan yang sah ketika sebuah instance perlu membawa kita ke anggota static: properti constructor milik setiap instance menunjuk ke class pembuatnya. Kombinasi keduanya bekerja persis seperti akses langsung:

js
class KartuAnggota {
  constructor(nama, poin) {
    this.nama = nama;
    this.poin = poin;
  }

  static dariPendaftaran(nama, poin) {
    return new KartuAnggota(nama, poin);
  }
}

const kartuBudi = new KartuAnggota("Budi", 100);
console.log(kartuBudi.constructor.dariPendaftaran === KartuAnggota.dariPendaftaran);

Output yang Dihasilkan:

text
true

Perbandingan menghasilkan true karena kartuBudi.constructor dan KartuAnggota menunjuk function yang sama. Pola ini berguna di dalam method instance yang perlu membuat instance baru sejenis. Meski begitu, untuk kebanyakan kasus akses langsung lewat nama class lebih mudah dibaca — pakailah jalur constructor hanya di dalam kode class itu sendiri.

Sebelum melanjutkan, mari kita rapikan seluruh peta penyimpanan anggota yang terkumpul sepanjang section ini. Sebuah class punya tiga rumah untuk anggotanya, dan kini ketiganya sudah kita jumpai:

RumahCara mendeklarasikanDijangkau lewatContoh
Field instancethis.poin = ... di constructor, atau field biasaInstance: kartuBudi.poinData yang berbeda tiap anggota
Method instanceMethod biasa di badan classInstance lewat prototype: kartuBudi.namaPerilaku yang dipakai bersama
Anggota staticDiawali static di badan classClass: KartuAnggota.totalDibuatPenghitung, konstanta, factory

Kunci pemilihan rumahnya satu: tanyakan milik siapa data atau perilaku itu. Milik satu anggota tertentu — rumah pertama. Milik semua instance secara sama — rumah kedua. Milik class sebagai entitas — rumah ketiga.

Static Factory: Satu Pintu Pembuatan Instance #

Method dariPendaftaran menampilkan alasan paling umum anggota static dibuat: mengubah data mentah menjadi instance. Input dari form, berkas, atau API biasanya berupa teks dan kurang lengkap; factory adalah tempat yang tepat untuk membersihkan, memberi nilai cadangan, dan memutuskan aturan sebelum new dijalankan.

Pemanggil pun lebih jelas membacanya. KartuAnggota.dariPendaftaran(...) mengatakan niatnya — “buat kartu dari data pendaftaran” — sedangkan new KartuAnggota(...) hanya mengatakan mekanismenya. Ketika aturan pendaftaran berubah, satu factory yang diperbaiki, dan seluruh pemanggil ikut aman.

Kapan static Bukan Jawabannya #

Karena mudah dijangkau, anggota static bisa tergelincir menjadi tempat parkir kebiasaan lama: variabel global dengan nama baru. Perbedaannya tipis tapi nyata — penghitung totalDibuat aman karena ia menghitung peristiwa class itu sendiri, sedangkan penghitung “jumlah transaksi hari ini” adalah data bisnis yang berubah-ubah dan sebaiknya dikelola instance yang bertanggung jawab, misalnya class LaporanHarian.

Dua pertanyaan penyaring sebelum menulis static: apakah nilainya relevan meski belum ada satu pun instance? dan apakah nilainya sama untuk seluruh program, bukan untuk satu pengguna atau satu sesi? Bila salah satunya tidak terpenuhi, data itu lebih baik kembali ke instance. Anggota static yang sedikit dan bermakna lebih mudah dibaca daripada class yang jadi gudang serbaguna.

Class kita kini memiliki data privat, constructor yang menyiapkan instance, dan anggota static yang menandai milik class. Satu celah kecil tersisa pada pengalaman pemakainya: mengatur properti masih berarti menyentuh field mentah, sementara validasi terbaik seharusnya berjalan persis saat data diisi. Sintaks get dan set di chapter berikutnya menutup celah itu.

Berkas Sumber

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

kartu-anggota.js

Unduh berkas
javascript
// kartu-anggota.js
// static: anggota yang menempel di class, bukan di tiap instance.

class KartuAnggota {
  static totalDibuat = 0;
  static bonusPendaftaran = 20;

  constructor(nama, poin) {
    this.nama = nama;
    this.poin = poin + KartuAnggota.bonusPendaftaran;
    KartuAnggota.totalDibuat += 1;
  }

  static dariPendaftaran(nama, teksPoin) {
    const poin = Number(teksPoin) || 0;
    return new KartuAnggota(nama, poin);
  }
}

const kartuBudi = new KartuAnggota("Budi", 100);
const kartuSiti = KartuAnggota.dariPendaftaran("Siti", "85");

console.log(kartuBudi.poin);
console.log(kartuSiti.poin);
console.log(KartuAnggota.totalDibuat);

console.log(kartuBudi.dariPendaftaran);
console.log(kartuBudi.bonusPendaftaran);
console.log(KartuAnggota.bonusPendaftaran);

console.log(Object.hasOwn(KartuAnggota, "dariPendaftaran"));
console.log(Object.hasOwn(kartuBudi, "dariPendaftaran"));