JavaScript'te her nesnenin prototip adı verilen başka bir nesneye gizli bir bağlantısı vardır. Bir özelliği okumak istediğinizde motor önce nesnenin kendisine, bulamazsa prototipine, sonra prototipin prototipine bakar ve null değerine ulaşana kadar devam eder. Bu arama yoluna prototip zinciri denir ve JavaScript'teki kalıtımın tamamı bu mekanizmaya dayanır.

Sınıflar (class) bu modelin üzerine kurulmuş bir sözdizimidir, yeni bir kalıtım sistemi getirmez. Bu yazıda zincirin nasıl çalıştığını, Object.create() ve constructor fonksiyonlarını ve bunların sınıflarla ilişkisini çalıştırılmış örneklerle gösteriyoruz. Sınıf sözdiziminin kendisi için class yazısına bakabilirsiniz.

Özellik araması zincirde nasıl ilerler?

Özellik araması zincirde nasıl ilerler?

Spesifikasyon nesnenin prototipini [[Prototype]] adlı iç bir yuva olarak tanımlar. Bu yuvayı Object.getPrototypeOf() ile okur, Object.setPrototypeOf() ile değiştirirsiniz. Nesne değişmezi içinde __proto__: x anahtarı da prototipi belirlemenin standart bir yoludur (eski obj.__proto__ erişimcisinden farklıdır).

javascript
const arac = { tekerlek: 4, tur: "araç" };
const motosiklet = { tekerlek: 2, __proto__: arac };

console.log(motosiklet.tekerlek); // 2 (kendi özelliği, prototiptekini gölgeler)
console.log(motosiklet.tur); // araç (prototipten geldi)
console.log(motosiklet.renk); // undefined (zincirin sonuna kadar arandı)

console.log(Object.getPrototypeOf(motosiklet) === arac); // true
console.log(Object.getPrototypeOf(arac) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

Buradaki zincir şöyledir: motosiklet, arac, Object.prototype, null. Nesnenin kendisinde de prototipinde de aynı isimli özellik varsa kendisininki kazanır; buna özellik gölgeleme (property shadowing) denir. Bir nesneye değer atadığınızda ise (setter devreye girmiyorsa) her zaman nesnenin kendi özelliği oluşur, prototip değişmez.

Miras alınan metotlarda this

Prototipten gelen bir fonksiyon çağrıldığında this, fonksiyonun tanımlandığı prototipi değil, çağrının yapıldığı nesneyi gösterir. Kalıtımı kullanışlı kılan da budur: tek bir metot, her nesnenin kendi verisiyle çalışır.

javascript
const ebeveyn = {
  deger: 2,
  hesapla() {
    return this.deger + 1;
  },
};
const cocuk = { __proto__: ebeveyn };

console.log(cocuk.hesapla()); // 3 (deger, ebeveynden okundu)
cocuk.deger = 4; // cocuk üzerinde kendi özelliği oluşur
console.log(cocuk.hesapla()); // 5
console.log(ebeveyn.deger); // 2

Constructor fonksiyonları ve prototype özelliği

Burada iki farklı şeyi karıştırmamak önemli. Object.getPrototypeOf(obj) bir nesnenin prototipidir. Fonksiyon.prototype ise bir fonksiyonun sıradan bir özelliğidir ve o fonksiyon new ile çağrıldığında oluşan örneklerin prototipi olarak kullanılır. Varsayılan olarak bu nesnede, fonksiyonun kendisini gösteren bir constructor özelliği bulunur.

javascript
function Kutu(deger) {
  this.deger = deger;
}
Kutu.prototype.getir = function () {
  return this.deger;
};

const k1 = new Kutu(1);
const k2 = new Kutu(2);
console.log(k1.getir === k2.getir); // true (tek kopya, paylaşılıyor)
console.log(Object.getPrototypeOf(k1) === Kutu.prototype); // true
console.log(Kutu.prototype.constructor === Kutu); // true

// Prototip sonradan değişirse mevcut örnekler de etkilenir
Kutu.prototype.getir = function () {
  return this.deger * 10;
};
console.log(k1.getir()); // 10

Arrow function'ların varsayılan bir prototype özelliği yoktur; bu yüzden constructor olarak kullanılamazlar:

javascript
console.log(typeof function () {}.prototype); // object
console.log((() => {}).prototype); // undefined

Object.create() ile prototipi doğrudan belirlemek

Object.create(proto), prototipi proto olan yeni ve boş bir nesne döndürür. İkinci parametre olarak özellik tanımlayıcıları (property descriptor) verebilirsiniz; bu sayede nesne değişmeziyle yapılamayan şeyler, örneğin numaralandırılamayan veya yazılamayan özellikler, oluşturma anında tanımlanabilir. proto ne null ne de bir nesneyse TypeError fırlatılır.

javascript
const kisiProto = {
  tanit() {
    return `Ben ${this.ad}`;
  },
};
const ali = Object.create(kisiProto);
ali.ad = "Ali";
console.log(ali.tanit()); // Ben Ali

// Prototipsiz nesne: Object.prototype metotlarını miras almaz
const sozluk = Object.create(null);
sozluk.elma = "apple";
console.log(sozluk.hasOwnProperty); // undefined
console.log(Object.hasOwn(sozluk, "elma")); // true

// Tanımlayıcı ile: varsayılan olarak yazılamaz ve numaralandırılamaz
const sabit = Object.create({}, { kod: { value: 42 } });
console.log(sabit.kod, Object.keys(sabit)); // 42 []

try {
  Object.create(5);
} catch (e) {
  console.log(e.constructor.name); // TypeError
}

Object.create(null) ile oluşan nesne, anahtarları kullanıcıdan gelen sözlükler için kullanışlıdır çünkü toString gibi miras alınmış isimlerle çakışma olmaz. Bu tür nesnelerde obj.hasOwnProperty() bulunmadığı için Object.hasOwn() kullanın. Anahtar-değer saklamak için başka bir seçenek de Map ve Set yapılarıdır.

Sınıflar ve prototip zinciri aynı şeydir

Sınıflar ve prototip zinciri aynı şeydir: Nesne değişmezi { __proto__: p }, Object.create(p, tanımlar), new Fonksiyon(), class ... extends

class Derived extends Base yazdığınızda arka planda şu zincir kurulur: örnek, Derived.prototype, Base.prototype, Object.prototype, null. Sınıf gövdesindeki metotlar Derived.prototype üzerinde durur. Bunu doğrudan kontrol edebilirsiniz:

javascript
class Base {}
class Derived extends Base {
  selam() {
    return "selam";
  }
}
const d = new Derived();

console.log(typeof Derived); // function
console.log(Object.getPrototypeOf(d) === Derived.prototype); // true
console.log(Object.getPrototypeOf(Derived.prototype) === Base.prototype); // true
console.log(Object.hasOwn(Derived.prototype, "selam")); // true
console.log(Object.getPrototypeOf(Derived) === Base); // true

Son satır, static üyelerin neden kalıtımla geçtiğini açıklıyor: türetilmiş sınıfın (fonksiyonun) kendi prototipi üst sınıftır. Aynı zinciri sınıf kullanmadan kurmak isterseniz MDN, Object.setPrototypeOf() ile mevcut prototip nesnesini bağlamayı önerir:

javascript
function Base() {}
Base.prototype.kim = function () {
  return "Base";
};
function Derived() {
  Base.call(this);
}
Object.setPrototypeOf(Derived.prototype, Base.prototype);

const o = new Derived();
console.log(o.kim()); // Base
console.log(o instanceof Base); // true
console.log(o.constructor === Derived); // true

Eski kodlarda sık görülen Derived.prototype = Object.create(Base.prototype) kalıbı da çalışır, ancak prototype nesnesini değiştirdiği için constructor özelliğini kaybettirir; MDN bu yüzden onu hataya daha açık bulur.

YöntemPrototip ne zaman belirlenirNot
Nesne değişmezi { __proto__: p }Oluşturma anındaStandart, kısa sözdizimi
Object.create(p, tanımlar)Oluşturma anındanull prototip ve tanımlayıcı desteği
new Fonksiyon()Oluşturma anındaFonksiyon.prototype kullanılır
class ... extendsOluşturma anındaEn okunaklı yol, özel alan desteği
Object.setPrototypeOf(o, p)SonradanMotor optimizasyonlarını bozabilir
obj.__proto__ = pSonradanStandart dışı ve kullanımdan kaldırılmış

Kendi özellik mi, miras mı?

for...in döngüsü zincirdeki tüm numaralandırılabilir özellikleri gezer; in operatörü de zincirin tamamına bakar. Yalnızca nesnenin kendi özelliklerini istiyorsanız Object.keys() veya Object.hasOwn() kullanın. Bir özelliğin değerinin undefined olması, özelliğin olmadığı anlamına gelmez; bu yüzden varlık kontrolünü değerle yapmayın.

javascript
const ust = { miras: 1 };
const alt = { kendi: 2, __proto__: ust };

for (const anahtar in alt) console.log(anahtar);
// kendi
// miras
console.log(Object.keys(alt)); // [ 'kendi' ]
console.log("miras" in alt, Object.hasOwn(alt, "miras")); // true false

instanceof aslında zinciri sorgular

nesne instanceof F operatörü, F.prototype nesnesinin nesne'nin prototip zincirinin herhangi bir yerinde bulunup bulunmadığını test eder. Yani sorulan şey "bu nesneyi F mi oluşturdu?" değil, "F.prototype bu nesnenin zincirinde var mı?" sorusudur. Aynı kontrolü isPrototypeOf() ile de yapabilirsiniz. Bu fark, constructor hiç çalışmadan oluşturulan nesnelerde görünür hâle gelir:

javascript
class Hayvan {}
class Kedi extends Hayvan {}

const tekir = new Kedi();
console.log(tekir instanceof Hayvan); // true
console.log(Hayvan.prototype.isPrototypeOf(tekir)); // true

const sahte = Object.create(Kedi.prototype); // constructor çalışmadı
console.log(sahte instanceof Kedi); // true
console.log({} instanceof Kedi); // false

Bu nedenle instanceof sonucu, nesnenin constructor'da hazırlanan alanlara sahip olduğunu garanti etmez. Sınıf örneklerini kesin olarak tanımak gerekiyorsa sınıfa özel bir alan tanımlayıp #alan in nesne kontrolü yapmak daha sağlam bir yöntemdir; bu desen class yazısında gösteriliyor.

Yaygın hatalar

Yaygın hatalar: Yerleşik prototipleri genişletmek, Prototipi sonradan değiştirmek, Fonksiyon.prototype ile nesnenin prototipini karıştırmak, Derin zincirlerde arama maliyeti
  • Yerleşik prototipleri genişletmek: Array.prototype.benimMetodum = ... gibi eklemelere "monkey patching" denir. Dil ileride aynı isimle farklı davranan bir metot eklerse kodunuz bozulur. MDN tek meşru kullanımı, yeni bir özelliği eski motorlara taşıyan polyfill olarak görür.
  • Prototipi sonradan değiştirmek: Object.setPrototypeOf() çalışır ama motorların zincir için yaptığı optimizasyonları bozabilir. Mümkünse prototipi oluşturma anında belirleyin.
  • Fonksiyon.prototype ile nesnenin prototipini karıştırmak: Kutu.prototype, Kutu fonksiyonunun prototipi değildir; Object.getPrototypeOf(Kutu) sonucu Function.prototype olur.
  • Derin zincirlerde arama maliyeti: Var olmayan bir özelliğe erişim, zincirin sonuna kadar arama yapar. Performansın kritik olduğu yerlerde zinciri gereksiz uzatmayın.

Sık Sorulan Sorular

__proto__ kullanmak doğru mu?

Nesne değişmezi içindeki { __proto__: x } anahtarı standarttır. Buna karşılık obj.__proto__ erişimcisi standart dışı ve kullanımdan kaldırılmış kabul edilir; onun yerine Object.getPrototypeOf() ve Object.setPrototypeOf() kullanın.

class kullanıyorsam prototipi bilmem gerekir mi?

Günlük kodda doğrudan prototip yazmanız gerekmeyebilir, ancak this kaybı, instanceof sonuçları veya bir metodun nereden geldiği gibi sorunları ayıklarken zinciri bilmek büyük kolaylık sağlar.

Object.create(null) ne işe yarar?

Hiçbir şey miras almayan bir nesne üretir. Kullanıcı girdisini anahtar olarak kullanan sözlüklerde, constructor veya toString gibi miras alınmış isimlerle çakışmayı önler.

Bir özelliğin nesnenin kendisine ait olduğunu nasıl anlarım?

Object.hasOwn(nesne, "ad") kullanın. Prototipi null olan nesnelerde de çalışır. Nesne metotlarının geneli için Object metotları yazısına bakabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: JavaScript öğrenme rehberi · JavaScript class kullanımı · Object metotları · this anahtar kelimesi · Map, Set ve WeakMap · JavaScript nedir?