Mert Özen Her Satırda Daha İleri
Frontend JavaScript Orta — #07

JavaScript Class'ları: Constructor, Getter/Setter, Static ve Private Alanlar

Mert Özen 3 Kas 2026 9 dk 5 görüntülenme
JavaScript Class'ları: Constructor, Getter/Setter, Static ve Private Alanlar

Class syntax'ını kurabiye kalıbı benzetmesiyle kuruyor; constructor, getter/setter, static, # ile private alanlar, extends ve super'i örneklerle görüyor, class'ın aslında prototype olduğunu öğreniyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Aşağıda küçük bir sayaç class'ı var. Son satır konsola ne yazdırır?

class Counter {
  #count = 0;

  increment() {
    this.#count++;
    return this;
  }

  get value() {
    return this.#count;
  }
}

const c = new Counter();
c.increment().increment();
console.log(c.value, c.count);

Okumaya devam etmeden önce bir tahmin yap. İpucu vermeyeceğim ama şunu söyleyeyim: bu birkaç satırda bu yazının neredeyse tüm konuları var. Cevabı private alanları anlattıktan sonra konuşacağız.

Kurabiye Kalıbı: Class Neden Var?

Elinde bir kurabiye kalıbı olduğunu düşün. Kalıbın kendisi yenmez; ama onunla aynı şekilde yüzlerce kurabiye basabilirsin. Her kurabiyenin üstüne farklı bir süsleme yapabilirsin, yine de hepsi aynı kalıptan çıktığı için aynı biçimdedir. Class tam olarak bu kalıptır. Kalıptan çıkan her kurabiye de bir instance, yani o class'tan üretilmiş bir object'tir.

Daha önce object'leri object literal ile tek tek yazdık. Bir kullanıcı için sorun yok, ama aynı yapıda yüz kullanıcı ve hepsinde aynı method'lar gerekiyorsa, her seferinde aynı kodu kopyalamak hem hata kaynağı hem de bellek israfıdır. Class syntax ES2015 ile dile geldi ve bu işi düzenli, okunur bir biçimde yapmamızı sağlıyor.

constructor, Method'lar ve Alanlar

En basit hâliyle bir class şöyle görünür. Bunu tarayıcının konsoluna yapıştırıp dene:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  greet() {
    return `Hi, I'm ${this.name}`;
  }
}

const alice = new User("Alice", "alice@example.com");
const bob = new User("Bob", "bob@example.com");

console.log(alice.greet()); // "Hi, I'm Alice"
console.log(bob.name);      // "Bob"

new User(...) yazdığında engine arka planda şunları yapar: boş bir object oluşturur, bu object'i this olarak constructor'a verir, constructor'ın içindeki atamaları çalıştırır ve sonunda o object'i sana döndürür. constructor özel bir method'dur; her new çağrısında bir kez çalışır ve instance'ın ilk hâlini kurar.

greet gibi method'lar ise her instance'a ayrı ayrı kopyalanmaz. Hepsi tek bir yerde durur ve tüm instance'lar onu paylaşır. alice.greet === bob.greet yazarsan true görürsün. Kalıbın kendisi bir kez var; kurabiyeler onu kullanıyor.

ES2022 ile constructor'a dokunmadan doğrudan class gövdesine alan (field) yazabiliyoruz. Varsayılan değeri olan property'ler için çok daha temiz:

class Task {
  done = false;
  createdAt = new Date();

  constructor(title) {
    this.title = title;
  }

  toggle() {
    this.done = !this.done;
  }
}

const t = new Task("Finish the blog post");
t.toggle();
console.log(t.done); // true

Method'larla alanlar arasındaki farkı aklında tut: alanlar her instance'ın kendi üzerinde oluşur, method'lar paylaşılır. Bu ayrıntı biraz sonra tuzaklar bölümünde işimize yarayacak.

Getter ve Setter: Property Gibi Görünen Method'lar

Bazen bir değeri saklamak yerine o an hesaplamak istersin. Ya da bir property'ye değer atanırken araya girip kontrol yapmak istersin. get ve set tam bunun için var:

class Temperature {
  constructor(celsius) {
    this.celsius = celsius;
  }

  get fahrenheit() {
    return this.celsius * 9 / 5 + 32;
  }

  set fahrenheit(value) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77

temp.fahrenheit = 212;
console.log(temp.celsius);    // 100

Dikkat et, temp.fahrenheit yazarken parantez kullanmadık. Dışarıdan bakan biri bunun sıradan bir property olduğunu sanar; içeride ise her okumada bir hesap yapılıyor. Termostat gibi düşün: ekranda gördüğün sayı bir yerde yazılı durmuyor, sensörden o an okunuyor.

Setter'ın en sık kullanımı doğrulamadır. Daha önce gördüğümüz hata yönetimiyle birleştirince geçersiz değeri kapıda durdurabilirsin:

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price; // setter burada da çalışır
  }

  get price() {
    return this._price;
  }

  set price(value) {
    if (typeof value !== "number" || value < 0) {
      throw new RangeError("Price cannot be negative");
    }
    this._price = value;
  }
}

const pen = new Product("Pen", 15);
pen.price = -5; // RangeError: Price cannot be negative

Burada değeri _price adında ayrı bir property'de tuttuk. Alt çizgi eskiden "buna dokunma" demenin bir gelenekti ama gerçek bir koruma sağlamıyor; pen._price = -5 yazan herkes kontrolü atlar. Gerçek korumayı birazdan # ile göreceğiz.

static: Instance'a Değil, Class'a Ait Olanlar

Bazı method'lar tek bir kullanıcıyla değil, kullanıcı kavramının kendisiyle ilgilidir. Bir JSON'dan kullanıcı üretmek ya da toplam kaç kullanıcı oluşturulduğunu saymak gibi. Bunlar static ile class'ın üzerine yazılır:

class User {
  static count = 0;

  constructor(name) {
    this.name = name;
    User.count++;
  }

  static fromJSON(json) {
    const data = JSON.parse(json);
    return new User(data.name);
  }
}

const u = User.fromJSON('{"name":"Zoe"}');
console.log(u.name);     // "Zoe"
console.log(User.count); // 1
console.log(u.fromJSON); // undefined

Konsolda aynı isimli bir class'ı ikinci kez tanımlarken "has already been declared" hatası alırsan sayfayı yenile ve öyle yapıştır. Static üyelere instance üzerinden ulaşamazsın, sadece class adıyla çağırırsın. Zaten hep kullandığın Math.max(), Array.from(), Number.isNaN() da birer static method'dur. Kimse new Math() yazmaz.

Static kurulumun birkaç satırdan uzun sürdüğü durumlar için ES2022 static block'u getirdi. Class tanımlandığı anda bir kez çalışır ve içindeki this class'ın kendisidir:

class Config {
  static defaults;

  static {
    try {
      this.defaults = JSON.parse('{"theme":"dark","lang":"en"}');
    } catch {
      this.defaults = { theme: "light", lang: "en" };
    }
  }
}

console.log(Config.defaults.theme); // "dark"

Private Alanlar ve Method'lar: # ile Gerçek Gizlilik

ES2022 ile gelen # öneki, alt çizgi geleneğinin aksine gerçek bir duvar örer. # ile başlayan bir alan ya da method sadece o class'ın gövdesi içinden erişilebilir:

class BankAccount {
  #balance = 0;

  deposit(amount) {
    this.#check(amount);
    this.#balance += amount;
  }

  withdraw(amount) {
    this.#check(amount);
    if (amount > this.#balance) {
      throw new Error("Insufficient funds");
    }
    this.#balance -= amount;
  }

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

  #check(amount) {
    if (amount <= 0) throw new RangeError("Amount must be positive");
  }
}

const account = new BankAccount();
account.deposit(500);
account.withdraw(120);
console.log(account.balance);  // 380
console.log(account.#balance); // SyntaxError

Son satır bir HTML dosyasındaki script'te çalıştırıldığında kod daha çalışmadan SyntaxError verir. Yani engine, class dışından #balance'a dokunma girişimini çalıştırma anında değil, kodu okurken yakalar. Bir not düşeyim: Chrome'un konsolu hata ayıklamayı kolaylaştırmak için private alanları okumana izin verebilir. Bu sadece DevTools'un bir ayrıcalığı; gerçek kodda bu satır asla çalışmaz.

Bir bankanın kasasını düşün. Bakiyeni gişeden sorabilir, para yatırıp çekebilirsin; ama kasaya girip rakamı elle değiştiremezsin. Gişe görevlisi, yani deposit ve withdraw, her işlemde kuralları kontrol eder.

Şimdi baştaki soruya dönelim. c.value bir getter, private #count'u okuyor ve iki kez artırıldığı için 2 döndürüyor. c.count ise bambaşka bir şey: object'te count adında normal bir property yok, o yüzden undefined geliyor. #count ile count iki ayrı isimdir. Cevap: 2 undefined. increment()'in this döndürmesi de zincirleme çağrıyı mümkün kıldı.

Bir object'in belirli bir private alana sahip olup olmadığını #balance in obj ile sınayabilirsin. Bu da ES2022'de geldi ve sadece class içinde yazılabilir.

extends ve super: Bir Class'tan Yenisini Türetmek

Bir class'ı genişletip ona yeni yetenekler eklemek için extends kullanırız. Alt class, üst class'ın tüm method'larını alır; istersen bazılarını kendi versiyonuyla değiştirebilirsin:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  speak() {
    return `${super.speak()}: Woof!`;
  }
}

const rex = new Dog("Rex", "Beagle");
console.log(rex.speak());
// "Rex makes a sound: Woof!"

console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

super iki farklı şekilde kullanılır. Constructor içinde super(name), üst class'ın constructor'ını çağırır. Method içinde super.speak() ise üst class'taki aynı isimli method'a ulaşır; böylece üst class'ın işini tamamen silmeden üstüne ekleme yapabilirsin.

instanceof, bir object'in belirli bir class'tan, ya da onun soyundan gelip gelmediğini söyler. Rex hem bir köpek hem de bir hayvan.

Private alanlar inheritance'ta farklı davranır. Üst class'ta tanımlanan #balance alt class'ın instance'ında da vardır, ama alt class'ın kodu ona doğrudan this.#balance diye erişemez. Her class'ın private alanları kendine aittir. Alt class'ın ihtiyacı varsa üst class bir getter ya da method sunmalı.

Perde Arkası: Class Aslında Bir Fonksiyon

Konsolda şunu dene:

class User {
  greet() {
    return "Hello";
  }
}

console.log(typeof User);                    // "function"
console.log(typeof User.prototype.greet);    // "function"
console.log(Object.getPrototypeOf(new User()) === User.prototype); // true

Class, JavaScript'e yeni bir object modeli getirmedi. Altında hep var olan prototype mekanizmasının üstüne giydirilmiş daha okunur bir syntax. Method'ların instance'lar arasında paylaşılmasının sebebi de bu: hepsi User.prototype üzerinde duruyor ve instance'lar onları prototip zinciri üzerinden buluyor. Bu zincirin nasıl çalıştığını İleri seride derinlemesine inceleyeceğiz; şimdilik "class, prototype'ın kibar yüzüdür" demek yeterli.

Sık Düşülen Tuzaklar

İlki new'i unutmak. Class bir fonksiyon olsa da sıradan bir fonksiyon gibi çağrılamaz:

class User {}
User(); // TypeError: Class constructor User cannot be invoked without 'new'

İkincisi alt class'ta super()'dan önce this kullanmak. Alt class'ın constructor'ında this, ancak üst class kendi kurulumunu bitirdikten sonra hazır olur. Yukarıdaki Animal class'ını kullanarak dene:

class Cat extends Animal {
  constructor(name) {
    this.lives = 9; // ReferenceError
    super(name);
  }
}

new Cat("Tabby");

Üçüncüsü, daha önce this yazısında gördüğümüz method kaybı. Bir method'u callback olarak başka bir yere verdiğinde object'le bağı kopar:

class Timer {
  seconds = 0;

  tick() {
    this.seconds++;
    console.log(this.seconds);
  }
}

const timer = new Timer();
setTimeout(timer.tick, 1000); // NaN, çünkü this artık timer değil

Çözüm ya setTimeout(() => timer.tick(), 1000) ile sarmak ya da method'u arrow function bir alan olarak tanımlamak: tick = () => { this.seconds++; }. Arrow function alanı this'i instance'a sabitler; bedeli, bu fonksiyonun paylaşılmayıp her instance'a ayrı ayrı oluşturulmasıdır.

Dördüncüsü, class'ı tanımlamadan önce kullanmak. Class declaration'ları da let ve const gibi TDZ'dedir, function declaration gibi yukarı taşınmış hâlde kullanılamaz. Son olarak: class gövdesi her zaman strict mode'da çalışır, yani "use strict" yazmasan da kurallar geçerlidir.

Küçük Bir Deneme

★☆☆ Book adında bir class yaz: title, author ve pages alsın. summary() method'u "Frankenstein, Mary Shelley, 280 pages" gibi bir string döndürsün. İki farklı kitap üretip ikisinin de summary method'unun aynı fonksiyon olduğunu === ile doğrula.

★★☆ Çalıştırmadan önce çıktıyı tahmin et:

class Box {
  static created = 0;
  #size;

  constructor(size) {
    this.#size = size;
    Box.created++;
  }

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

const a = new Box(3);
const b = new Box(5);
a.size = 10;
console.log(a.size, b.size, Box.created, a.created);

★★★ BankAccount'u genişleten bir SavingsAccount yaz. Constructor'ı bir faiz oranı alsın ve addInterest() method'u bakiyeye faizi eklesin. #balance'a doğrudan erişemeyeceğini göreceksin; çözümü üst class'ın public method'larını kullanarak bul.

Bu yazıdaki örneklerin hepsi senkron çalıştı: bir satır bitti, sonraki başladı. Ama gerçek dünyada kod bazen bir sunucudan cevap beklemek, bir zamanlayıcının dolmasını beklemek zorunda kalıyor. Bir sonraki yazıda JavaScript'in tek bir thread ile bu beklemeleri sayfayı dondurmadan nasıl yönettiğini, call stack ve event loop'la tanışarak göreceğiz.