Sınıflar baÅka sınıfları geniÅletebilirler. Bunun için prototip kalıtımı tabanlı güzel bir yazılıÅı bulunmaktadır.
DiÄer bir sınıftan kalıtım saÄlamak için "extends" ile belirtmek gerekmektedir.
AÅaÄıda Animalâdan kalıtım alan Rabbit sınıfı gösterilmektedir:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stopped.`);
}
}
// Inherit from Animal
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!
extends kelimesi aslında Rabbit.prototypeâdan referans alıp bunun [[Prototype]]'ını Animal.prototypeâa ekler. Aynen daha önce de gördüÄümüz gibi.
Artık rabbit hem kendi metodlarına hem de Animal metodlarına eriÅebilir.
extendsâten sonra her türlü ifade kullanılabilir.Extendsâten sonra sadece sınıf deÄil her türlü ifade kullanılabilir.
ÃrneÄin, üst sınıfı yaratan yeni bir fonksiyon çaÄrısı:
function f(phrase) {
return class {
sayHi() { alert(phrase) }
}
}
class User extends f("Hello") {}
new User().sayHi(); // Hello
Burada class User f("Hello")'nun sonucunu kalıtır.
Bu belki çok ileri teknik programlama kalıpları için kullanıÅlı olabilir. Böylece birçok koÅula göre fonksiyonları kullanarak farklı sınıflar oluÅturabilir ve bunlardan kalıtım alınabilir.
Bir metodu geçersiz kılma, üstüne yazma.
Åimdi biraz daha ileri gidelim ve metodun üstüne yazalım. Åimdiden sonra Rabbit stop metodunu kalıtım alır, bu metod this.speed=0âı Animal sınıfında ayarlamaya yarar.
EÄer Rabbit içerisinde kendi stop metodunuzu yazarsanız buna üstüne yazma denir ve Animalâda yazılmıŠstop metodu kullanılmaz.
class Rabbit extends Animal {
stop() {
// ... rabbit.stop() için artık bu kullanılacak.
}
}
â¦Fakat genelde üst metodun üzerine yazmak istenmez, bunun yerine küçük deÄiÅiklikler yapmak veya fonksiyonliteyi geniÅletmek daha fazla tercih edilen yöntemdir. Metodda birçeyler yapar ve genelde bundan önce/sonra veya iÅlerken üst metodu çaÄırırız.
Sınıflar bunun için "super" anahtar kelimesini saÄlarlar.
-
super.method(...)üst classâın metodunu çaÄırmak için. -
super(...)üst metodun yapıcısını (constructor) çaÄırmak için kullanılır.
ÃrneÄin, Rabbit otomatik olarak durduÄunda gizlensin.
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stopped.`);
}
}
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
stop() {
super.stop(); // üst sınıfın stop metodunu çaÄır.
this.hide(); // sonra gizle
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stopped. White rabbit hides!
Artık Rabbit, stop metodunda üst sınıfın super.stop()'unu çaÄırmaktadır.
superâi bulunmamaktadır.Ok fonksiyonları bölümünde bahsedildiÄi gibi, ok fonksiyonlarının superâi bulunmamaktadır.
EÄer eriÅim olursa bu super dıÅarıdaki fonksiyonundur. ÃrneÄin:
class Rabbit extends Animal {
stop() {
setTimeout(() => super.stop(), 1000); // üst'ün stop'unu 1 sn sonra çaÄır.
}
}
Ok fonksiyonu içerisindeki super ile stop() içerisine yazılan super aynıdır. EÄer âsıradanâ bir fonksiyon tanımlarsak bu hataya neden olabilir:
// Unexpected super
setTimeout(function() { super.stop() }, 1000);
Yapıcı metodu ezmek.
Yapıcı metodlar ile yapılan Åeyler biraz çetrefillidir.
Åimdiye kadar Rabbit kendisine ait yapıcıâya sahipti.
Åartnameâye göre eÄer bir sınıf diÄer baÅka bir sınıftan türer ve constructorâa sahip deÄil ise aÅaÄıdaki yapıcı otomatik olarak oluÅturulur.
class Rabbit extends Animal {
// yapıcısı olmayan ve türetilen sınıf için oluÅturulur.
constructor(...args) {
super(...args);
}
}
GördüÄünüz gibi aslında üst sınıfın yapıcıâsını tüm argümanları göndererek çaÄırır. EÄer kendimiz bir yapıcı yazmazsak bu meydana gelir.
Ãzel olarak uyarlanmıŠbir yapıcı oluÅturalım. Bu isim ile birlikte earLengthâi de tanımlasın:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}
// ...
}
// ÃalıÅmaz!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
Nasıl ya! Hata aldık. Åimdi de rabbit oluÅturamıyoruz. Neden peki?
Kısa cevap: TüremiÅ sınıftaki yapıcı kesinlikle super(...) i çaÄırmalıdır. Bu thisâden önce olmalıdır.
â¦Peki neden? Ãok garip deÄilmi?
Tabi bu açıklanabilir bir olay. Detayına girdikçe daha iyi anlayacaksınız.
JavaScriptâte âtüreyen sınıfın yapıcı fonksiyonuâ ve diÄerleri arasında farklılıklar mevcuttur. TüremiÅ sınıflarda eÅ yapcıı fonksiyonlar içsel olarak [[ConstructorKind]]:"derived" Åeklinde etiketlenir.
Farklılık:
- Normal yapıcı çalıÅtıÄında boÅ bir objeyi
thisolarak yaratır ve bunun ile devam eder. - Fakat türemiÅ sınıfın yapıcısı çalıÅtıÄında bunu yapmaz. Ãst fonksiyonun yapıcısının bunu yapmasını bekler.
EÄer kendimiz bir yapıcı yazarsak bundan dolayı super i çaÄırmamız gerekmektedir. Aksi halde this referansı oluÅturulmaz ve biz de hata alırız.
Rabbitâin çalıÅabilmesi için thisâden önce super() çaÄırılmalıdır.
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
// ...
}
// Åimdi düzgün bir Åekilde çalıÅır.
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10
Super: dahililer, [[HomeObject]]
Artık superâin derinliklerine dalma vakti geldi. Altında yatan ilginç Åeyler nelermiÅ bunları göreceÄiz.
Ãncelikle, Åimdiye kadar öÄrendiklerimizle super ile çalıÅmak mümkün deÄil.
Ever gerçekten, kendimize soralım, nasıl teknik olarak böyle bir Åey çalıÅabilir? Bir obje metodu çalıÅtıÄında var olan objeyi this olarak alır. EÄer biz super.method()'u çaÄırırsak metodâu nasıl alabilir? DoÄal olarak methodâu var olan objenin prototipinden almak gerekmektedir. Peki teknik olarak bunu JavaScript motoru nasıl halledebilir?
Belki thisin [[Prototype]]'ını this.__proto__.method olarak alıyordur? Malesef böyle çalıÅmıyor.
Bunu test edelim. Sınıflar olmadan basit objelerle, fazladan karmaÅıklaÅtırmadan deneyelim.
AÅaÄıda rabbit.eat(), kendisinin üst metodu animal.eat()'i çaÄırmalıdır:
let animal = {
name: "Animal",
eat() {
alert(this.name + " eats.");
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() {
// bizim tahminimze göre super.eat() bu Åekilde çalıÅabilir.
this.__proto__.eat.call(this); // (*)
}
};
rabbit.eat(); // Rabbit eats.
(*) satırında animal prototipinden eatâi almakta ve var olan obje kaynaÄından çaÄırmaktayız. Dikkat edin burada .call(this) oldukça önemlidir. Ãünkü basit this.__proto__.eat() üst eatâi prototipin kaynaÄı ile çaÄırır, var olan objenin deÄil.
Yukarıdaki kod beklendiÄi gibi çalıÅmaktadır. DoÄru alert vermektedir.
Åimdi bu zincire bir tane daha obje ekleyelim. İÅler nasıl bozuluyor görelim:
let animal = {
name: "Animal",
eat() {
alert(this.name + " eats.");
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...tavÅan-stili ayla ve üst sınıfı çaÄır.
this.__proto__.eat.call(this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...uzun kulaklar ile bir Åeyler yap ve üst sınıfı çaÄır.
this.__proto__.eat.call(this); // (**)
}
};
longEar.eat(); // Error: Maximum call stack size exceeded
YazdıÄınız kod artık çalıÅmıyor! longEar.eat()'i çaÄırırken hata olduÄunu görebilirsiniz.
Bu çok açık olmayabilir, fakat longEar.eat() in hata kodlarını takip ederseniz nedenini anlayabilirsiniz. (*) ve (**) satırlarında this var olan (longEar) objesidir. Hatırlayın: Tüm objeclerin metodları this olarak var olan objeyi alır, prototipini deÄil.
Ãyleyse, (*),(**) ve this.__proto__ tamamen aynıdır: rabbit. Hepsi rabbit.eatâi sonsuz zincire çıkmadan çaÄırır.
AÅaÄıda ne olduÄunu daha iyi anlatan bir görsel bulunmakta:
-
longEar.eat()içerisinde(**)satırırabbit.eatâithis=longEarolarak çaÄırmakta.// longEar.eat() içerisinde this = longEar Åeklinde kullanmaktayız. this.__proto__.eat.call(this) // (**) // olur longEar.__proto__.eat.call(this) // bu da rabbit.eat.call(this); -
Sonra
rabbit.eatâin(*)satırı içerisinde bu zinciri daha üstlere çıkarmaya çalıÅıyoruz, fakatthis=longEar, yanithis.__proto__.eatyinerabbit.eat!// rabbit.eat() içerisinde thiss= longEar bulunmakta this.__proto__.eat.call(this) // (*) // olur longEar.__proto__.eat.call(this) // veya (yine) rabbit.eat.call(this); -
⦠Artık
rabbit.eat'in kendisini neden sonsuz defa çaÄırdıÄını görmüŠolduk.
Problem sadece this kullanılarak çözülemez.
[[HomeObject]]
Buna bir çözüm saÄlamak için, JavaScript fonksiyonlar için bir tane dahili özellik eklemiÅtir: [[HomeObject]]
Bir fonksiyon sınıf veya obje metodu olarak tanımlandıÄında, bunun [[HomeObject]]'i kendisi olur
Bu aslında baÄımsız fonksiyonlar fikrini bozmaktadır, çünkü metodlar kendi objelerini hatırlamaktadır. Ayrıca [[HomeObject]] deÄiÅtirilemez, yani bu baÄ sonsuza kadardır. Aslında bu dilde yapılan oldukça büyük bir deÄiÅiklik.
Fakat bu deÄiÅiklik güvenlidir. [[HomeObject]] sadece üst sınıfın metodlarını superâde çaÄırmaya yarar. Bundan dolayı uyumluluÄu bozmaz.
Åimdi super ile nasıl çalıÅıyor bunu inceleyelim --tekrardan, sade objeleri kullanalım:
let animal = {
name: "Animal",
eat() { // [[HomeObject]] == animal
alert(this.name + " eats.");
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // [[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // [[HomeObject]] == longEar
super.eat();
}
};
longEar.eat(); // Long Ear eats.
Her metod kendi objesinin [[HomeObject]] özelliÄini hatırlamakta. Sonra superbunu üst objenin prototipini çözerken kullanır.
[[HomeObject]] sınıflar veya sade objelerâde tanımlanan metodlar için tanımlanır. Fakat objeler için, metodlar aynen Åu Åekilde tanımlanmalıdır: method(), "method:function()" Åeklinde deÄil.
AÅaÄıdaki örnekte karÅılaÅtırma için metod-olmayan yazım kullanılmıÅtır. [[HomeObject]] özelliÄi tanımlanmadı bundan dolayı da kalıtım çalıÅmayacaktır.
let animal = {
eat: function() { // kısa yazım: eat() {...} olmalıdır.
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
rabbit.eat(); // super'i çalıÅtırırken hata oldu çünkü [[HomeObject]] bulunmamakta.
It works as intended, due to [[HomeObject]] mechanics. A method, such as longEar.eat, knows its [[HomeObject]] and takes the parent method from its prototype. Without any use of this.
Methods are not âfreeâ
As weâve known before, generally functions are âfreeâ, not bound to objects in JavaScript. So they can be copied between objects and called with another this.
The very existance of [[HomeObject]] violates that principle, because methods remember their objects. [[HomeObject]] canât be changed, so this bond is forever.
The only place in the language where [[HomeObject]] is used â is super. So, if a method does not use super, then we can still consider it free and copy between objects. But with super things may go wrong.
Hereâs the demo of a wrong super call:
let animal = {
sayHi() {
console.log(`I'm an animal`);
}
};
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
console.log("I'm a plant");
}
};
let tree = {
__proto__: plant,
sayHi: rabbit.sayHi // (*)
};
tree.sayHi(); // I'm an animal (?!?)
A call to tree.sayHi() shows âIâm an animalâ. Definitevely wrong.
The reason is simple:
- In the line
(*), the methodtree.sayHiwas copied fromrabbit. Maybe we just wanted to avoid code duplication? - So its
[[HomeObject]]israbbit, as it was created inrabbit. Thereâs no way to change[[HomeObject]]. - The code of
tree.sayHi()hassuper.sayHi()inside. It goes up fromrabbitand takes the method fromanimal.
Methods, not function properties
[[HomeObject]] is defined for methods both in classes and in plain objects. But for objects, methods must be specified exactly as method(), not as "method: function()".
The difference may be non-essential for us, but itâs important for JavaScript.
In the example below a non-method syntax is used for comparison. [[HomeObject]] property is not set and the inheritance doesnât work:
let animal = {
eat: function() { // should be the short syntax: eat() {...}
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
rabbit.eat(); // Error calling super (because there's no [[HomeObject]])
Summary
- To extend a class:
class Child extends Parent:- That means
Child.prototype.__proto__will beParent.prototype, so methods are inherited.
- That means
- When overriding a constructor:
- We must call parent constructor as
super()inChildconstructor before usingthis.
- We must call parent constructor as
- When overriding another method:
- We can use
super.method()in aChildmethod to callParentmethod.
- We can use
- Internals:
- Methods remember their class/object in the internal
[[HomeObject]]property. Thatâs howsuperresolves parent methods. - So itâs not safe to copy a method with
superfrom one object to another.
- Methods remember their class/object in the internal
Also:
- Arrow functions donât have own
thisorsuper, so they transparently fit into the surrounding context.
Yorumlar
<code>kullanınız, birkaç satır eklemek için ise<pre>kullanın. EÄer 10 satırdan fazla kod ekleyecekseniz plnkr kullanabilirsiniz)