Her Nesnenin Bir Prototype'ı Vardır
JavaScript, prototype tabanlı bir dildir. Kulağa biraz egzotik gelse de aslında fikir gayet basit: her nesnenin başka bir nesneye giden gizli bir bağlantısı vardır — işte bu bağlantının ucundaki nesneye prototype diyoruz. Nesnede olmayan bir özelliği istediğinizde, JavaScript bu bağlantıyı takip edip oraya sorar.
rabbit nesnesinin kendisine ait bir eats özelliği yok. JavaScript önce rabbit'e bakıyor, bulamayınca prototype bağlantısını takip edip animal'a gidiyor, orada eats: true'yu buluyor ve onu döndürüyor. flies için de zinciri baştan sona dolaşıyor, hiçbir yerde bulamıyor ve undefined dönüyor.
İşin tamamı aslında bu "bak, bulamazsan yukarı çık" mantığından ibaret. Kalıtım, metotlar, class yapısı — hepsi bu mekanizmanın üstüne kurulu.
Prototype zinciri nedir?
Zincir tek adımda bitmiyor. Bir prototype'ın da kendi prototype'ı olabilir, onun da başka bir prototype'ı... ta ki null'a ulaşana kadar:
Çalıştırdığında önce rabbit, sonra Object.prototype, en sonunda da null göreceksin. İşte bu yüzden rabbit.toString() çağrısı, sen toString diye bir şey tanımlamamış olmana rağmen sorunsuz çalışır — çünkü bu metot Object.prototype üzerinde yaşıyor ve neredeyse her zincirin en tepesinde o duruyor.
Özellik araması bu zinciri aşağıdan yukarıya doğru tarar. Atama ise tam tersine, her zaman nesnenin kendisine yazar — yukarı doğru tırmanmaz. Bu asimetri önemli bir detay ve insanları sık sık şaşırtıyor.
Constructor fonksiyonları ve .prototype
class diye bir yapı JavaScript'e gelmeden önce, birbirine benzeyen çok sayıda nesne üretmenin standart yolu, new ile çağrılan bir constructor function yazmaktı:
new User("Ada") çağrıldığında iki şey olur:
- Yeni bir nesne oluşturulur ve onun prototype'ı
User.prototypeolarak ayarlanır. Userfonksiyonu,thisbu yeni nesneye bağlı şekilde çalışır.
greet her örneğin içine kopyalanmıyor. Tek bir yerde, yani User.prototype üzerinde yaşıyor; hem ada hem de boris ona zincirlerini yukarı doğru takip ederek ulaşıyor. Son satırın true basmasının sebebi de bu — ikisi de tam anlamıyla aynı fonksiyon.
prototype ile __proto__ arasındaki fark
Bu iki isim herkesin kafasını karıştırır. Akraba kavramlar ama aynı şey değiller.
User.prototype, constructor fonksiyonunun bir özelliğidir.new User(...)ile üretilen örneklerin prototype'ı haline gelen nesnedir.ada.__proto__(ya daObject.getPrototypeOf(ada)), örneğin kendisinde bulunan ve prototype'ına yukarı doğru işaret eden bağdır.
Yeni yazdığın kodlarda obj.__proto__ yerine Object.getPrototypeOf(obj) kullanmayı tercih et. __proto__, geriye dönük uyumluluk için hâlâ duran eski bir erişimci; asıl resmi API ise bu fonksiyon.
Class'lar Aslında Prototype'ın Şekerlenmiş Hali
Modern JavaScript sana class yazma imkânı veriyor ama perde arkasında hâlâ prototype'larla iş görüyorsun. İki sürümü yan yana koyup karşılaştıralım:
greet metodu, elle yazsaydınız düşeceği yerle aynı yere, yani User.prototype üzerine düştü. class anahtar kelimesi aslında size çoğunlukla daha derli toplu bir sözdizimi, daha katı kurallar (new kullanmak zorundasınız) ve extends için daha temiz bir yol sunar — ama çalışma zamanı modeli birebir aynıdır.
Bunu bilmek, hata mesajlarını okurken veya this ile uğraşırken işinize yarar. "User.prototype.greet" diye bir hata gördüğünüzde bu tuhaf bir iç isim değildir — metodun gerçekten yaşadığı yerdir.
Kalıtım Aslında Sadece Daha Uzun Zincirlerdir
extends, bir prototipi bir diğerine bağlar. Ebeveynin prototipi, çocuğun prototipinin prototipi hâline gelir:
rex.eat çağrıldığında JavaScript önce rex nesnesine bakar, bulamayınca Dog.prototype'a, oradan da Animal.prototype'a geçer. eat metodunu orada bulur ve this hâlâ rex'e bağlıyken çalıştırır. extends aslında tam olarak bunu yapıyor — prototype zincirini senin için kuruyor.
Object.create ile Doğrudan Prototype'lı Nesne Oluşturma
Aslında constructor'a hiç ihtiyacın yok. Object.create(proto) sana, belirttiğin prototype'a sahip yeni bir nesne verir:
class yok, new yok, constructor fonksiyonu da yok. Paylaşılan bir prototype üzerinden tek bir metodu ortak kullanan iki nesne var, hepsi bu. İşte prototypal inheritance'ın en çıplak hâli — gerisi hep bunun üstüne kurulu.
hasOwnProperty: Kendi özelliği mi, miras mı?
Property araması zincir boyunca yukarı çıktığı için, "foo" in obj ifadesi miras alınan property'ler için de true döner. Nesnenin gerçekten kendisine ait olan bir property'yi ayırt etmen gerektiğinde Object.hasOwn (ya da eski adıyla hasOwnProperty) kullan:
name instance üzerinde, greet ise prototype üzerinde yaşıyor. in operatörü ikisini de bulur; Object.hasOwn ise yalnızca ilkini görür. Bu ayrım, nesneleri for...in ile dolaşırken ya da serialize ederken kritik hale gelir — çoğu zaman sadece nesnenin kendi property'lerini istersin.
Built-in Prototype'lara Monkey-Patch Yapmayın
Array.prototype programındaki her dizi tarafından paylaşıldığı için, teoride ona yeni metotlar ekleyebilirsin:
// Lütfen yapmayın.
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3
Sorun bunun çalışmaması değil — gayet de çalışıyor. Asıl sorun şu: her kütüphane, her bağımlılık ve JavaScript'in gelecekteki her sürümü artık o aynı isim alanını sizinle paylaşıyor. Array.prototype.last bir gün gerçekten dile eklendiğinde — üstelik biraz farklı bir davranışla — sizin (veya bir başkasının) kodu fark edilmesi zor şekillerde bozulur. Array.prototype.flatten / Array.prototype.flat hikâyesi bu konudaki klasik ibret vakasıdır.
Yardımcıları bağımsız fonksiyonlar olarak tutun:
Çarpışabileceğin ortak bir yüzey daha az demek.
Zihinsel Model
Diğer her şeyi bir kenara bırakırsan, prototype konusu aslında üç kurala iner:
- Her nesnenin bir prototype bağlantısı vardır (bazen
nullolabilir). - Özellik okumaları bu zinciri yukarı doğru takip eder; yazma işlemleri etmez.
class,newveextends, kendi elinleObject.createyazmadan bu zincirleri kurmanın yollarıdır.
Bu üçünü aklında tuttuğun sürece this'in davranışı, instanceof, metot çözümlemesi ve kalıtım her şey yerli yerine oturur.
Sırada: Event Loop
Prototype'lar nesne modelini tamamlıyor. Bir sonraki bölümde tamamen farklı bir konuya geçiyoruz: JavaScript'in kodunu zaman içinde nasıl çalıştırdığına. Event loop; zamanlayıcıların, promise'lerin ve async/await'in neden o şekilde davrandığını belirleyen mekanizma ve asenkron çalışan her şeyin temelini oluşturuyor.
Sıkça Sorulan Sorular
JavaScript'te prototype nedir?
JavaScript'teki her nesnenin, prototype adı verilen başka bir nesneye giden dahili bir bağlantısı vardır. Nesnenin kendisinde bulunmayan bir özelliğe eriştiğinizde, JavaScript bu bağlantıyı — yani prototype zincirini — yukarı doğru takip ederek özelliği arar. Bir kez tanımlanan metotların birçok örnek (instance) arasında paylaşılabilmesini işte bu zincir sağlar.
__proto__ ile prototype arasındaki fark nedir?
prototype, constructor fonksiyonlarında (ve class'larda) bulunan bir özelliktir. new ile oluşturulan örneklerin prototype'ı haline gelecek nesnedir. __proto__ (veya Object.getPrototypeOf(obj)) ise bir örneğin kendi prototype'ına işaret eden gerçek bağlantıdır. Yani instance.__proto__ === Constructor.prototype eşitliği geçerlidir.
JavaScript class'ları prototype'ın şekerlenmiş hali mi?
Büyük ölçüde evet. class Foo { bar() {} } yazdığınızda bar, tıpkı function Foo(){} ve Foo.prototype.bar = function(){} yazmış gibi Foo.prototype üzerine yerleşir. Class'lar; private alanlar, daha sıkı semantik kurallar ve extends ile super için daha temiz bir sözdizimi getirir — ama arka plandaki mekanizma hâlâ prototype'lardır.
Array.prototype gibi yerleşik prototype'lara metot eklemeli miyim?
Neredeyse hiçbir zaman. Array.prototype veya Object.prototype üzerinde yapacağınız değişiklikler, kütüphanelerden gelenler dahil programınızdaki tüm dizileri ve nesneleri etkiler. İleride dile eklenecek özelliklerle çakışabilir ve for...in döngülerini bozabilir. Kendi yardımcı fonksiyonlarınızı ayrı fonksiyonlarda ya da modüllerde tutun.