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

JavaScript Prototype Zinciri: Object'ler Birbirinden Nasıl Miras Alır?

Mert Özen 3 Kas 2026 9 dk 7 görüntülenme
JavaScript Prototype Zinciri: Object'ler Birbirinden Nasıl Miras Alır?

[[Prototype]] bağlantısını, zincirde property aramasını, __proto__ ile Object.getPrototypeOf farkını, constructor function, Object.create ve class'ın altında yatan prototype mekanizmasını konsolda adım adım çözüyoruz.

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

Önce Tahmin Et

Temel ve Orta serilerde epey yol yürüdük: class'lar, closure ve this, promise'ler, async/await, fetch, DOM, event'ler ve module'ler. İleri seride perdenin arkasına geçiyoruz. Object'lerin birbirine nasıl bağlandığını, event loop'un gerçek çalışma sırasını, belleği, performansı, worker'ları ve Web Components'ı konuşacağız. İlk durak, JavaScript'in object modelinin kalbi olan prototype.

Her zamanki gibi bir soruyla başlayalım. Bu kod konsola ne yazdırır?

const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;

console.log(rabbit.eats);
console.log(Object.keys(rabbit));
console.log("eats" in rabbit);

Okumaya devam etmeden önce bir tahmin yap. rabbit'e hiçbir zaman eats yazmadık, yine de üç satırın üçü de bir şey döndürecek. Hangisinin neden döndüğünü zinciri anlattıktan sonra göreceğiz.

Her Object'in Gizli Bir Bağlantısı Var

JavaScript'te her object'in, spesifikasyonda [[Prototype]] diye yazılan gizli bir iç alanı vardır. Bu alan ya başka bir object'i ya da null'ı gösterir. Gösterdiği object'e o object'in prototype'ı denir. Prototype'ın da kendi prototype'ı olabilir; object'ler böylece birbirine bağlanıp bir zincir oluşturur.

Bunu bir aile gibi düşün. Bir yemek tarifi aradığında önce kendi defterine bakarsın. Orada yoksa annene sorarsın, onda da yoksa anneannene. Zincirin sonunda soracak kimse kalmadıysa cevap "bilmiyoruz" olur. JavaScript'te bu "bilmiyoruz"un karşılığı undefined'dır.

Bu zinciri farkında olmadan her gün kullanıyorsun. Konsola yapıştırıp dene:

const user = { name: "Ada" };

console.log(user.toString()); // "[object Object]"
console.log(Object.getPrototypeOf(user) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

user object'ine hiçbir zaman toString diye bir method eklemedik. Method, zincirin bir üst halkası olan Object.prototype'tan geliyor. Object.prototype'ın prototype'ı ise null; zincir orada biter. Array'lerde zincir bir halka daha uzundur: [] önce Array.prototype'a, o da Object.prototype'a bağlanır. map, push, includes gibi method'ların hepsi Array.prototype'ta yaşar; her array onları kopyalamadan, zincir üzerinden kullanır.

__proto__ mu, Object.getPrototypeOf mu?

Eski kodlarda ve forum cevaplarında prototype'a obj.__proto__ ile ulaşıldığını çok görürsün. Çalışır, ama MDN bu erişimciyi kullanımdan kaldırılmış (deprecated) olarak işaretler. Aslında __proto__ object'in kendi property'si bile değildir; Object.prototype üzerinde tanımlı bir getter/setter çiftidir. Tarayıcılar onu yıllar önce kendi başlarına eklediği için geriye uyumluluk adına standarda alındı.

Bugün kullanman gereken araçlar şunlar:

const base = { greet() { return "hi"; } };
const obj = {};

Object.setPrototypeOf(obj, base);
console.log(Object.getPrototypeOf(obj) === base); // true
console.log(obj.greet()); // "hi"

const dict = Object.create(null);
console.log(dict.__proto__); // undefined
console.log(Object.getPrototypeOf(dict)); // null

Son iki satır __proto__'ya neden güvenmemen gerektiğini gösteriyor. Prototype'ı null olan bir object Object.prototype'tan miras almadığı için __proto__ erişimcisine de sahip değildir. Object.getPrototypeOf ise her durumda doğru cevabı verir.

Bir uyarı daha: var olan bir object'in prototype'ını Object.setPrototypeOf ile sonradan değiştirmek, motorun o object için yaptığı optimizasyonları bozar ve yavaştır. Prototype'ı object'i oluştururken belirle; bunun için Object.create var. Object literal'i içinde { __proto__: base } yazmak ise ayrı bir syntax'tır, geçerlidir ve kullanımdan kaldırılmamıştır.

Zincirde Arama: Motor Ne Yapıyor?

Bir property'yi okuduğunda motor önce object'in kendi property'lerine bakar. Bulamazsa [[Prototype]]'a geçer ve orada arar. Bulana ya da null'a varana kadar yukarı çıkmaya devam eder. null'a vardıysa sonuç undefined'dır.

Şimdi baştaki soruya dönebiliriz. rabbit.eats true döner, çünkü rabbit'te olmayan eats bir üst halkada, yani animal'da bulunur. Object.keys(rabbit) ise yalnızca ["jumps"] döner: Object.keys zincire hiç bakmaz, sadece object'in kendi ve sayılabilir (enumerable) property'lerini listeler. "eats" in rabbit yine true, çünkü in operatörü bütün zinciri tarar. "Bu property gerçekten bu object'in mi?" diye sormak istiyorsan ES2022 ile gelen Object.hasOwn'u kullan.

Okuma zincirde yukarı çıkar, yazma çıkmaz. Bunu da konsolda dene:

const bird = {
  flies: true,
  move() {
    return `${this.name} is flying`;
  }
};
const sparrow = Object.create(bird);
sparrow.name = "Tweety";

console.log(sparrow.move()); // "Tweety is flying"

sparrow.flies = false;
console.log(sparrow.flies, bird.flies); // false true
console.log(Object.hasOwn(sparrow, "flies")); // true

move method'u bird'de duruyor ama içindeki this sparrow'u gösteriyor. Daha önce gördüğümüz kural burada da geçerli: this, method'un nerede bulunduğuna değil, çağrıda noktanın solunda kimin olduğuna bakar. Bu sayede binlerce object tek bir method'u paylaşır ve her biri kendi verisiyle çalışır.

sparrow.flies = false satırı ise bird'e dokunmadı. Atama, sparrow'un üzerinde kendine ait yeni bir flies oluşturdu ve prototype'taki değer gölgede kaldı. Buna shadowing denir. Bunun iki istisnası var: zincirde aynı isimde bir setter ya da yazılamaz (non-writable) bir property bulunması. İkisini de bir sonraki yazıda property descriptor'larla birlikte göreceğiz.

Bir not: for...in döngüsü de zincirdeki sayılabilir property'leri gezer. Object'leri dolaşırken Object.keys ya da Object.entries'i tercih etmemizin bir sebebi bu.

Constructor Function ve prototype Property'si

Class syntax'ı gelmeden önce object üretmenin standart yolu constructor function'dı. Bugün kendin yazmasan bile okuyacağın pek çok kod hâlâ böyle görünüyor:

function User(name) {
  this.name = name;
}

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

const linus = new User("Linus");
const grace = new User("Grace");

console.log(linus.greet()); // "Hi, I'm Linus"
console.log(linus.greet === grace.greet); // true
console.log(Object.getPrototypeOf(linus) === User.prototype); // true

new User("Linus") dediğinde motor dört iş yapar. Boş bir object oluşturur. Bu object'in [[Prototype]]'ını User.prototype'a bağlar. User fonksiyonunu, this yeni object'i gösterecek şekilde çalıştırır. Fonksiyon kendisi başka bir object döndürmediyse yeni object'i döndürür.

İsimlendirme burada kafa karıştırır, o yüzden dikkat: User.prototype, User fonksiyonunun prototype'ı değildir. O, new User() ile üretilecek object'lerin prototype'ı olacak object'tir. Fonksiyonun kendi prototype'ı Function.prototype'tır. Normal fonksiyonlar bu prototype property'si ile birlikte doğar ve içinde fonksiyonun kendisini gösteren bir constructor bulunur, yani User.prototype.constructor === User. Arrow function'ların prototype'ı yoktur; bu yüzden new ile çağrılamazlar.

greet'i constructor'ın içinde this.greet = function () {} diye tanımlasaydık her object kendi kopyasını taşırdı. Prototype'a koyduğumuz için iki object aynı fonksiyonu paylaşıyor; linus.greet === grace.greet satırının true dönmesi bunun kanıtı.

Object.create: Prototype'ı Elle Seçmek

Object.create(proto), prototype'ı proto olan yepyeni ve boş bir object üretir. Ne constructor ne de new gerekir. Zinciri kurmanın en açık yolu budur:

const vehicle = {
  describe() {
    return `${this.brand}, ${this.wheels} wheels`;
  }
};

const car = Object.create(vehicle);
car.brand = "Togg";
car.wheels = 4;

const bike = Object.create(vehicle, {
  brand: { value: "Bisan", enumerable: true },
  wheels: { value: 2, enumerable: true }
});

console.log(car.describe()); // "Togg, 4 wheels"
console.log(bike.describe()); // "Bisan, 2 wheels"

İkinci argüman, property'leri descriptor'larla tanımlamana izin verir. enumerable gibi ayarların tam olarak ne yaptığını bir sonraki yazıda açacağız. Bir de Object.create(null) var: hiçbir şeyden miras almayan, tertemiz bir object. Eskiden kullanıcıdan gelen anahtarları saklamak için sözlük olarak kullanılırdı; bugün bu iş için çoğu zaman daha önce gördüğümüz Map daha doğru bir tercih.

Class Aslında Ne Yapıyor?

Daha önce gördüğümüz class syntax'ı yeni bir object modeli getirmedi; altta aynı prototype mekanizması çalışıyor. Kendin doğrula:

class Account {
  constructor(owner) {
    this.owner = owner;
  }
  info() {
    return `Account owner: ${this.owner}`;
  }
}

class SavingsAccount extends Account {
  rate() {
    return 0.4;
  }
}

const acc = new SavingsAccount("Ada");

console.log(typeof Account); // "function"
console.log(Object.getPrototypeOf(acc) === SavingsAccount.prototype); // true
console.log(Object.getPrototypeOf(SavingsAccount.prototype) === Account.prototype); // true
console.log(Object.hasOwn(Account.prototype, "info")); // true
console.log(acc instanceof Account); // true

Zincir şöyle: acc → SavingsAccount.prototype → Account.prototype → Object.prototype → null. acc.info() çağrısında motor info'yu acc object'inde bulamaz, SavingsAccount.prototype'ta da bulamaz, Account.prototype'ta bulur. instanceof da sihir yapmaz; Account.prototype'ın acc object'inin zincirinde bir yerde geçip geçmediğine bakar.

Yine de class ile constructor function birebir aynı değil. Class method'ları sayılamaz (non-enumerable) olarak tanımlanır, bu yüzden for...in'de görünmez. Class gövdesi her zaman strict mode'da çalışır. Bir class'ı new olmadan çağırırsan TypeError alırsın. extends ayrıca static tarafı da bağlar: Object.getPrototypeOf(SavingsAccount) === Account da true döner ve static method'lar bu sayede miras alınır.

Built-in Prototype'lara Dokunma

Zinciri öğrenince insanın aklına hemen şu fikir gelir: Array.prototype'a bir method eklersem bütün array'ler onu kullanabilir.

Array.prototype.last = function () {
  return this[this.length - 1];
};

console.log([1, 2, 3].last()); // 3

Çalışıyor, ama yapma. Buna monkey patching denir ve geçmişte iz bırakmış sorunları var. Dile Array.prototype.contains eklenecekken, eski MooTools library'sinin aynı isimle eklediği method'la çakıştığı için isim includes oldu. 2018'de flatten önerisi yine aynı library yüzünden bazı siteleri bozunca method'un adı flat olarak değiştirildi; bu olay "SmooshGate" diye anılır. Yani senin eklediğin bir method yarın standarda girerse ya senin kodun bozulur ya da dil senin yüzünden isim değiştirmek zorunda kalır.

Üstelik bu şekilde eklenen property sayılabilir olduğu için for...in ile dolaşan her kodda aniden ortaya çıkar. Yardımcı fonksiyonunu ayrı bir fonksiyon ya da module olarak yaz. Tek meşru istisna polyfill'dir: standartta zaten tanımlı olan bir özelliği, henüz desteklemeyen eski bir tarayıcıya, spesifikasyona birebir uyarak eklemek. Denemen bitince konsolda delete Array.prototype.last yazıp ortalığı temizle.

Sık Düşülen Tuzaklar

İlk tuzak, prototype'a değiştirilebilir bir değer koymak. Method'lar için harika olan paylaşım, veri için felakettir:

function Team() {}
Team.prototype.members = [];

const red = new Team();
const blue = new Team();
red.members.push("Linus");

console.log(blue.members); // ["Linus"]

red.members.push(...) bir atama değil, bir okumadır. Önce members okunur, zincirde paylaşılan array bulunur, sonra o tek array değiştirilir. Her object'e özel veri constructor'da this'e yazılmalı.

İkincisi, prototype'a arrow function koymak. User.prototype.greet = () => this.name yazarsan, arrow function'ın kendi this'i olmadığı için object'i değil, tanımlandığı yerdeki this'i görür. Prototype method'ları için normal fonksiyon ya da class method'u kullan.

Üçüncüsü, prototype'ı toptan değiştirmek. User.prototype = { greet() {} } yazınca constructor property'si kaybolur ve daha önce oluşturulmuş object'ler eski prototype'a bağlı kalmaya devam eder. Method eklemek istiyorsan var olan object'e tek tek ekle.

Sonuncusu bir tasarım tuzağı: zincir uzadıkça hem arama hem de akıl yürütme uzar. Motorlar property aramasını çok iyi önbelleğe aldığı için performans nadiren sorun olur, ama beş altı katlı bir miras ağacını okumak ve değiştirmek gerçekten zordur. Inheritance'ı composition ile karşılaştırırken bu konuya geri döneceğiz.

Küçük Bir Deneme

★☆☆ Çalıştırmadan önce çıktıyı tahmin et, sonra dene: const a = { x: 1 }; const b = Object.create(a); const c = Object.create(b); a.x = 5; console.log(c.x, Object.hasOwn(c, "x")); Ardından c.x = 9 yaz ve a.x'in değişip değişmediğine bak.

★★☆ Bir object'in prototype zincirini array olarak döndüren chainOf(obj) fonksiyonunu yaz. Object.getPrototypeOf'u null gelene kadar çağırman yeterli. [], {}, Object.create(null) ve kendi yazdığın bir class'tan üretilen object'le dene; her halkanın constructor.name değerini yazdır.

★★★ Yukarıdaki Account ve SavingsAccount class'larını class kelimesini kullanmadan, constructor function ve Object.create ile yeniden yaz. Hem acc instanceof Account hem de acc.constructor === SavingsAccount true dönsün. İpucu: ikincisi için constructor'ı elle yerine koyman gerekecek.

Bu yazıda property'leri okurken ve yazarken zincirin nasıl davrandığını gördük, ama property'lerin kendisinin de görünmeyen ayarları olduğunu söyleyip geçtik. Bir sonraki yazıda o ayarları açıyoruz: bir property'yi salt okunur yapmak, listelerden gizlemek, silinemez kılmak ve Object.freeze ile bir object'i gerçekte ne kadar dondurabildiğimiz.