And you think you’re so clever and classless and free. — John Lennon
JavaScript, sınıfsız, nesne yönelimli bir dildir ve bu nedenden dolayı klasik kalıtım (classical inheritance) yerine prototip kalıtımı kullanır. Bu, C++ ve Java gibi geleneksel nesne yönelimli dillere alışkın programcılar için kafa karıştırıcı olabilir. JavaScript’in prototip kalıtımı, birazdan göreceğimiz gibi, klasik kalıtımdan daha fazla ifade gücüne sahiptir.
Ama öncelikle, kalıtım konusunu niye önsemsiyoruz ki? Her şeyden önce bunun iki nedeni var. Birincisi tip konusunda sağlanan kolaylıktır. Dil sisteminin benzer sınıfların referanslarını otomatik olarak yaymasını (cast) istiyoruz. Nesne referanslarının tip çevirimlerini açık (explicit) ve rutin bir şekilde gerektiren bir tip sistemi çok zayıf bir tip-güvenliği sağlar. Bu durum sıkı-tipli dillerde kritik öneme sahiptir, fakat nesne referanslarının çevirimlere ihtiyaç duymadığı JavaScript gibi serbest-tipli dillerde bunun önemi yoktur.
Java | JavaScript |
---|---|
Sıkı-tipli (Strongly-typed) | Serbest-tipli (Loosely-typed) |
Statik | Dinamik |
Klasik | Prototip |
Sınıflar | Fonksiyonlar |
Yapıcılar (Constructor) | Fonksiyonlar |
Metodlar | Fonksiyonlar |
İkinci neden kodun yeniden kullanılabilir olması. Tamamen aynı metodları uygulayan çok sayıda nesneye sahip olmak oldukça yaygındır. Sınıflar, hepsini tek bir tanım kümesinden oluşturabilmeyi sağlar. Bazı diğer nesnelere benzeyen, ancak sadece az sayıda metodun eklenmesi veya değiştirilmesinde farklılık gösteren nesnelere sahip olmak da yaygındır. Klasik kalıtım bunun için yararlıdır, ancak prototip kalıtım daha da yararlıdır.
Bunu göstermek için, geleneksel bir klasik dile benzeyen bir tarzda yazmamıza izin verecek bir sugar (örnek) (yazar burada “syntactic sugar” referansı veriyor) kullanacağız.
Klasik Kalıtım (Classical Inheritance)
İlk olarak, value
değişkeni için set
ve get
metodları ve value
değerini parantezlerle saracak toString
metodu olan bir Parenizor
sınıfı oluşturacağız.
function Parenizor(value) {
this.setValue(value);
}
Parenizor.method('setValue', function (value) {
this.value = value;
return this;
});
Parenizor.method('getValue', function () {
return this.value;
});
Parenizor.method('toString', function () {
return '(' + this.getValue() + ')';
});
Bu sözdilimi biraz garip gelebilir, ancak içindeki klasik kalıbın (classical pattern) farkına kolayca varılabilir. method
metodu, bir metot ismi ve fonksiyon alıp sınıfa genel metot olarak ekler.
Bu şekilde yazabiliriz:
myParenizor = new Parenizor(0);
myString = myParenizor.toString();
Tahmin edeceğiniz gibi, myString
değişkeninin değeri "(0)"
olacak.
Şimdi, Parenizor
sınıfından kalıt alacak (inherit) başka bir sınıf oluşturacağız, tek farkı ise value
sıfır veya boş olduğunda toString
metodu "-0-"
değerini dönecek.
function ZParenizor(value) {
this.setValue(value);
}
ZParenizor.inherits(Parenizor);
ZParenizor.method('toString', function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
});
Buradaki inherits
metodu Java’daki extends
ifadesine benzer. uber
metodu ise Java’nın super
metoduna benzer. Bir metodun üst sınıfın bir metodunu çağırmasına izin verir. (Rezerve edilmiş kelime kısıtlamalarını önlemek için isimler değiştirilmiştir.)
Bu şekilde kullanabiliriz:
myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString();
Bu sefer ise, myString
değişkeninin değeri "-0-"
olacak.
JavaScript’te sınıflar yoktur, ancak varmış gibi programlayabiliriz.
Çoklu Kalıtım (Multiple Inheritance)
Bir fonksiyonun prototype
nesnesini manipüle ederek birden çok sınıfın metodlarından oluşturulmuş bir sınıf oluşturmamıza izin veren çoklu kalıtımı uygulayabiliriz. Karışık çoklu kalıtımı uygulamak zor olabilir ve potansiyel olarak metot ismi çakışmalarına maruz kalınabilir. JavaScript’te karışık çoklu kalıtımı uygulayabiliriz, ancak bu örnek için Swiss Inheritance adında daha disiplinli bir yapı kullanacağız.
value
değişkeninin belirli bir aralıkta bir sayı olup olmadığını kontrol eden setValue
metoduna sahip, gerekirse exception fırlatan bir NumberValue
sınıfı olduğunu varsayalım. ZParenizor
sınıfımız için sadece setValue
ve setRange
metodlarını istiyoruz. Kesinlikle onun toString
metodunu istemiyoruz. Bu şekilde yazacağız:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
Bu yalnızca istenen metodları sınıfımıza ekler.
Parazit Kalıtım (Parasitic Inheritance)
ZParenizor
sınıfını yazabileceğimiz başka bir yol daha var. Parenizor
sınıfından kalıt almak yerine, Parenizor
yapıcısını (constructor) çağıran ve sonucu kendisininki gibi aktaran bir yapıcı yazıyoruz. Ve yapıcı, genel metodlar eklemek yerine, ayrıcalıklı metodlar ekler.
function ZParenizor2(value) {
var that = new Parenizor(value);
that.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-"
};
return that;
}
Klasik kalıtımda is-a ilişkisi var, ve parazit kalıtımda ise was-a-but-now’s-a ilişkisi var. Nesnenin yapımında yapıcının daha büyük bir rolü vardır. super
metodunun karşılığı olan uber
metodunun ayrıcalıklı metodlar için hala erişilebilir olduğunu görebilirsiniz.
Sınıf Çoğaltması (Class Augmentation)
JavaScript’in dinamizmi, mevcut bir sınıfın metodlarına ekleme veya değiştirme yapmamıza izin verir. method
metodunu istediğimiz zaman çağırabiliriz, ve sınıfın mevcut ve gelecekteki tüm örnekleri (instance) bu metoda sahip olacaktır. Kelimenin tam anlamıyla herhangi bir zamanda bir sınıfı genişletebiliriz. Kalıtım geriye dönük olarak çalışır. Başka bir anlama gelen Java’nın extends
ifadesiyle karıştırmamak için buna Sınıf Çoğaltması diyoruz.
Nesne Çoğaltması (Object Augmentation)
Statik nesne-yönelimli dillerde, başka bir nesneden biraz farklı bir nesne istiyorsanız, yeni bir sınıf tanımlamanız gerekir. JavaScript’te, ek sınıflara ihtiyaç duymadan ayrı nesnelere metodlar ekleyebilirsiniz. Bunun muazzam bir gücü var çünkü çok daha az sınıf yazabilirsiniz ve yazdığınız sınıflar ise çok daha basit olabilir. JavaScript nesnelerinin hashtable gibi olduklarını hatırlayın. Yeni değerleri istediğiniz zaman ekleyebilirsiniz. Eğer verilen değer bir fonksiyon ise, bu metoda dönüşür.
Yani yukarıdaki örnekte bir ZParenizor
sınıfına hiç ihtiyacım yoktu. Basit bir şekilde örneğimi değiştirebilirdim.
myParenizor = new Parenizor(0);
myParenizor.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
};
myString = myParenizor.toString();
Kalıtımın herhangi bir formunu kullanmadan myParenizor
örneğimize bir toString
metodu ekledik.
Sugar
Yukarıdaki örneklerin işe yaraması için dört tane sugar (örnek) metodu yazdım. İlk olarak, bir sınıfa örnek (instance) metodu ekleyen bir method
metodu:
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
Bu, Function.prototype
nesnesine bir genel metot ekler ve böylelikle Sınıf Çoğaltmasıyla bütün fonksiyonlar ona erişebilecekler. Bir isim ve fonksiyon alıp, bir fonksiyonun prototype
nesnesine ekler. Ve this
döndürür. Herhangi bir değer döndürmesi gerekmeyen bir metot yazdığımda, genellikle this
döndürmesini sağlarım. Bu, kademeli (cascade-style) bir programlama tarzına izin verir.
Sırada ise bir sınıfın başka sınıftan geldiğini gösteren inherits
metodu var. Her iki sınıf da tanımlandıktan sonra çağrılmalıdır, fakat kalıtım alınan sınıfın metodları eklenmeden önce çağrılmalıdır.
Function.method('inherits', function (parent) {
this.prototype = new parent();
var d = {},
p = this.prototype;
this.prototype.constructor = parent;
this.method('uber', function uber(name) {
if (!(name in d)) {
d[name] = 0;
}
var f, r, t = d[name], v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d[name] += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d[name] -= 1;
return r;
});
return this;
});
Tekrar belirtmek adına, burada Function
fonksiyonunu çoğaltıyoruz. parent
sınıfının bir örneğini oluşturup yeni prototype
olarak kullanıyoruz. constructor
alanına da kalıtım alınacak parent
sınıfını atayarak düzeltiyoruz, ve prototype
nesnesine de uber
metodunu ekliyoruz.
uber
metodu parametre olarak verilen isimli metodu kendi prototype
nesnesinde arıyor. Bu fonksiyon, Parazit Kalıtım veya Nesne Çoğaltması örnekleri için çağrılır. Eğer Klasik Kalıtım yapıyorsak, fonksiyonu parent
sınıfının prototype
nesnesinde bulmamız gerekiyor. return
ifadesi, fonksiyonu çalıştırmak için açık olarak (explicit) this
‘i belirleyip parametre dizisini geçerek fonksiyonun apply
metodunu kullanır. Parametreler (eğer var ise) arguments
dizisinden alınır. Ne yazık ki arguments
gerçek bir dizi (array) olmadığından, dizinin slice
metodunu çalıştırabilmek için apply
metodunu tekrar kullanmak zorundayız.
Son olarak, Swiss
metodu:
Function.method('swiss', function (parent) {
for (var i = 1; i < arguments.length; i += 1) {
var name = arguments[i];
this.prototype[name] = parent.prototype[name];
}
return this;
});
Swiss
metodu arguments
dizisini döngüler. Her bir name
için parent
sınıfının prototype
nesnesinden yeni sınıfın prototype
nesnesine bir üye kopyalar.
Sonuç
JavaScript klasik bir dil gibi kullanılabilir, ancak aynı zamanda oldukça benzersiz bir ifade düzeyine sahiptir. Klasik Kalıtıma, Swiss Kalıtıma, Parazit Kalıtıma, Sınıf ve Nesne Çoğaltmasına baktık. Bu büyük yeniden kod kullanım kalıpları seti, Java’dan daha basit ve küçük olarak görülen bir dilden geliyor.
Klasik nesneler serttir. Sert bir nesneye yeni bir üye eklemenin tek yolu yeni bir sınıf oluşturmaktır. JavaScript’te nesneler yumuşaktır. Basit bir atama ile yumuşak bir nesneye yeni bir üye eklenebilir.
JavaScript’teki nesneler çok esnek olduğundan, sınıf hiyerarşileri hakkında farklı düşünmek isteyeceksiniz. Derin hiyerarşiler uygun değildir. Yüzeysel hiyerarşiler ise etkili ve anlamlıdır.
was-a-but-now’s-a
Yazar parazit kalıtım için was-a-but-now’s-a örneğini klasik kalıtımın is-a örneğinden yola çıkarak bir kelime oyunu yapmıştır. Özet olarak is-a, A sınıfının B sınıfından kalıt (inherit) alarak bir alt sınıfı olması, ve dolayısıyla B sınıfının A sınıfınının “supperclass“‘ı olması olarak açıklanabilir. was-a-but-now’s-a ise, A sınıfının B sınıfından kalıt (inherit) almak yerine, B sınıfının yapıcısını çağırarak sonucu A sınıfının bir metodunun sonucu gibi aktarılması olarak açıklanabilir.
Bu, Douglas Crockford‘un Classical Inheritance in JavaScript adlı orijinal yazısının çevirisidir.