24 Ocak 2022

Protitipsel Kalıtım

Programlarken genelde bir şeyi alır ve bunu genişletmek isteriz.

Örneğin, kullanici adında bir obje ve bunun özellikleri ve metodları olsun, bunu biraz düzenleyerek admin ve misafir gibi iki farklı obje oluşturmak isteriz. Yani kullanici objesini doğrudan kopyalamak veya metodlarını tekrardan uygulamak değil bunlar üzerinden yeni objeler yaratmak isteyebiliriz.

Prototip kalıtımı buna olanak sağlamaktadır.

[[Prototype]]

Javascript objeleri gizli bir özellik olan [[Prototype]] özelliğine sahiptirler. Bu null olabilir veya başka objeye referans verebilir. Referans verilen obje “prototip” olarak adlandırılır.

[[Prototip]]'in “büyülü” bir anlamı bulunmaktadır. Objeden bir özellik okunmak istendiğinde, ve bu obje bulunamadığında JavaScript bunu otomatik olarak prototip’ten alır. Programlamada buna prototip kalıtımı denir. Birçok dil özelliği ve programlama tekniği bunun üzerine kuruludur.

[[Prototpe]] gizli bir özelliktir, fakat bunu ayarlamanın birçok yolu vardır.

Bunlardan biri __proto__ kullanmaktır:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;

Aklınızda bulunsun __proto__ [[Prototype]] ile aynı değildir. Bunun için alıcı/ayarlayıcı ( getter/setter)'dır. Bunun hakkında ilerleyen bölümlerde daha fazla açıklama yapılacaktır fakat şimdilik __proto__ yeterlidir.

Örneğin rabbit adında bir özelliğe arasanız ve bu özellik yoksa, JavaScript bunu otomatik olarak animal'dan alır.

Örneğin:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// Artık her ikisini de rabbit'te bulabilirsiniz.
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

(*) satırında animal'ın rabbit in özleliği olması sağlanır

Sonrasında alert rabbit.eats (**)'i okur. Bu rabbit'te olmadığından JavaScript [[Prototype]]'ı takip eder ve bunu animal'in içerinde bulur.

Böylece “animalrabbit'in prototip’i veya "rabbit prototipsel olarak animal kalıtımını almıştır" diyebiliriz.

Diyelim ki animal'ın birçok özelliği ve metodu olsun, bunları otomatik olarak rabbit de kullanabilir. Bu çeşit özelliklere kalıtılmış özellikler denir.

Eğer animal'da bir metodumuz varsa bu metod rabbit tarafından çağırılabilir olmaktadır.

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// walk prototipten alınmıştır.
rabbit.walk(); // Animal walk

Metod prototipten otomatik olarak şu şekilde alınmıştır:

Prototip zinciri daha da uzun olabilir:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
}

// walk prorotip zincirinden alınmıştır.
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (rabbit'ten gelmekte)

Aslında iki tane kısıtlama bulunmaktadır:

  1. Referanslar kapalı devre olamaz. Böyle bir duurmda hata verir.
  2. __proto__'nun değeri ya obje olur ya da null Diğer türlüsü ( tüm ilkel veri tipleri ) görmezden gelinir.

Çok açık olsa da tekrar söylemekte yarar var. Bir obje sadece bir tane [[Prototype]]'a sahip olabilir. Bir objenin iki farklı objeden kalıtım alamaz.

Kuralların Okuması/Yazılması.

Prototip sadece özelliklerin okunması için kullanılır.

Veri özelliklerinin yazılma/silinme ( alıcı/ayarlayıcı değil) işi doğrudan obje üzerinden yapılır.

Aşağıdaki örnekte rabbit'e kendi walk metodu atanmıştır:

let animal = {
  eats: true,
  walk() {
    /* Bu metod rabbit tarafından kullanılmayacaktır. */
  }
};

let rabbit = {
  __proto__: animal
}

rabbit.walk = function() {
  alert("Rabbit! Bounce-bounce!");
};

rabbit.walk(); // Rabbit! Bounce-bounce!

Artık rabbit.walk() metodu doğrudan kendi içerisinde bulur ve çalıştırır. Prototip kullanmaz:

Alıcı/Ayarlayıcı için ise eğer özellik okunursa bu doğrudan prototipte okunur ve uyarılır.

Örneğin aşağıdaki admin.fullName özelliğine bakın:

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// Ayarlayıcılar uyarıldı!
admin.fullName = "Alice Cooper"; // (**)

(*) satırında admin.fullName özelliği user prototipinde alıcıya sahiptir. Bundan dolayı çağırılır. (**) satırında ise ayarlayıcıya sahip olduğundan bu da çağırılır.

“this”'in değeri

Yukarıdaki örnekte aklınıza şöyle bir soru gelebilir. set fullName(value) içerisinde this'in değeri nedir? this.name ve this.surname yazılan yerlerde admin mi yoksa user mı kullanılır?

Cevap basittir: this prototip tarafından hiçbir şekilde etkilenmez.

Metodun bulunduğu yerin önemi olmaksızın, metod çağrısında this her zaman noktadan önceki bölümdür.

Öyleyese aslında ayarlayıcı admin'i this olarak kullanır. user'ı değil.

Çok büyük bir objeye ve buna ait birçok metoda, kalıtıma sahip olabileceğimizden dolayı bu aslında çok önemli bir olaydır. Sonrasında büyük objenin değil kalıtılmış objelerin metodlarını çalıştırabilir ve bunların özelliklerini değiştirebiliriz.

Örneğin burada animal aslında “metod deposu”'nu temsil etmektedir. rabbit ise bunu kullanır.

rabbit.sleep() çağrısı rabbit üzerinde this.isSleeping'i ayarlar:

// animal metodları
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};

// rabbit.isSleeping'i modifiye eder.
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (prototipte böyle bir özellik bulunmamaktadır.)

Sonuç görseli:

Eğer bird, sname gibi animal'dan miras alan objelere sahip olsaydık bunlar da animal'in metodlarına erişebilirlerdi. Fakat her metoddaki this bağlı bulunduğu objeye göre çalışırdı. Yani noktadan önceki metoda göre, animal'e göre değil. Bundan dolayı ne zaman this'e veri yazılsa o objelerin içerisine yazılır.

Sonuç olarak metodlar paylaşılsa bile objelerin durumları paylaşılmaz.

Özet

  • JavaScript’te tüm objelerin gizli [[Prototype]]'ı bulunmaktaıd. Bu özellik ya başka bir objedir veya null'dur.
  • Erişmek için obj.__proto__ kullanılabilir. (elbette diğer yollar da mevcuttur, ilerde bunlara değineceğiz.)
  • [[Prototype]] tarafından temsil edilen objeye “prototip” denir.
  • Eğer bir obj'nin özelliğini okumak veya bir metodunu çağırmak istersek ve o metod yok ise JavaScript bunu prototipte bulmaya çalışır. Yazma/Silme operasyonları doğrudan obje üzerinde çalıştırılır. Özellik ayarlayıcı olmadığı sürece prototip kullanılmaz.
  • Eğer obj.method()'u çağırırsak ve method prototipten alınırsa this yine de obj'i temsil eder. Bundan dolayı metodlar her zaman o anki obje ile çalışırlar miras kalsalar bile.

Görevler

önem: 5

Aşağıda birkaç obje üreten ve bunlar üzerinde değişiklikler yapan kod bulunmaktadır.

Bu işlem süresince hangi değerler gösterilir?

let animal = {
  jumps: null
};
let rabbit = {
  __proto__: animal,
  jumps: true
};

alert( rabbit.jumps ); // ? (1)

delete rabbit.jumps;

alert( rabbit.jumps ); // ? (2)

delete animal.jumps;

alert( rabbit.jumps ); // ? (3)

3 tane cevap bulunmaktadır

  1. rabbit'ten true alınır.
  2. animal'den null alınır.
  3. Böyle bir özerllik olmadığından undefined alınır.

önem: 5

#Arama algoritması

Görev iki bölümden oluşmaktadır.

Bir objemiz var:

let head = {
  glasses: 1
};

let table = {
  pen: 3
};

let bed = {
  sheet: 1,
  pillow: 2
};

let pockets = {
  money: 2000
};
  1. __proto__ kullanarak prototipleri özellikleri pockets->bed->table->head gibi bir yolu takip edecek şekilde prototipleri atayınız. Örneğin pockets.pen 3 ( table'da bulunan ) olmalı, bed.glasses ise 1 ( head'de bulunmalı)
  2. Sizce glasses değerini pocket.glasses ile mi yoksa head.glasses ile mi almak daha hızlıdır?
  1. __proto__'yu ekleyelim:

    let head = {
      glasses: 1
    };
    
    let table = {
      pen: 3,
      __proto__: head
    };
    
    let bed = {
      sheet: 1,
      pillow: 2,
      __proto__: table
    };
    
    let pockets = {
      money: 2000,
      __proto__: bed
    };
    
    alert( pockets.pen ); // 3
    alert( bed.glasses ); // 1
    alert( table.money ); // undefined
  2. Modern JavaScript motorlarında, bir özelliği objeden veya prototypetan almasının bir farklılığı yoktur. Özelliğin nerede olduğunu hatırlar ve bunu bir sonraki talepte tekrar kullanabilirler.

    Örneğin, pockets.glasses glasses'ı nerede bulduğunu hatırlar. Bu durumda glasses head'de bulundu, bir sonraki sefere doğrudan orada arayacaktır. Ayrıca kodda herhangi bir değişiklik olduğunda kendi önbelleğini siler böylece optimizasyon güvenli olur.

önem: 5

animaldan türemiş bir rabbit'imizi var.

Eğer rabbit.eat() çağırılırsa hangi obje full özelliğini alır: animal mi yoksa rabbit mi?

let animal = {
  eat() {
    this.full = true;
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.eat();

Cevap: rabbit.

Çünkü this noktadan önceki objeyi verir. Bu durumda rabbit.eat() rabbit üzerinde değişikliğe neden olur.

Özelliğe bakma ve çalıştırma iki ayrı şeydir. rabbit.eat önce prototipte bulunur sonra this=rabbit ile çalıştırılır.

önem: 5

speedy ve lazy diye hamster'objesinden türemiş iki tane objemiz olsun.

Biz bir tanesini beslediğimizde, diğeri de full oluyor. Bunun nedeni nedir, nasıl düzeltilir?

let hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// Bu yemeği buldu
speedy.eat("apple");
alert( speedy.stomach ); // apple

// Bu neden buldu peki?
alert( lazy.stomach ); // apple

speedy.eat("apple") çağrısında ne oluyor isterseniz daha yakından inceleyelim

  1. (=hamster) protitipinde speedy.eat bulunur, sonra this=speedy olacak şekilde çalıştırılır. ( .dan önceki obje )

  2. Sonra this.stomach.push() stomach özelliğini bulmalı ve push'u çağırmalı. this içinde stomch'(=speedy) i araştırır fakat bulamaz.

  3. Sonra prototip bağını takip ederek hamster içinde stomach'i bulur.

  4. Bunun içindeki push u çalıştırır. Böylece prototip’in stomach'i çalışmış olur

Böylece tüm hamsterlar’ın bir tane stomach'i oluyor.

Her defaında prototip’ten stomach alındığında ve sonra stomach.push ile olduğu yerde modifiye eder.

Aklınızda bulunsun basit bir atamada this.stomach= gibi basit atamada gerçekleşmez.

let hamster = {
  stomach: [],

  eat(food) {
    // this.stomach.push yerine this.stomach'i ata.
    this.stomach = [food];
  }
};

let speedy = {
   __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// Speedy yemeği buldu
speedy.eat("apple");
alert( speedy.stomach ); // apple

// Lazy'nin stomach'i boş kaldı
alert( lazy.stomach ); // <nothing>

Now all works fine, because this.stomach= does not perform a lookup of stomach. The value is written directly into this object.

Also we can totally evade the problem by making sure that each hamster has his own stomach:

let hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster,
  stomach: []
};

let lazy = {
  __proto__: hamster,
  stomach: []
};

// Speedy one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple

// Lazy one's stomach is empty
alert( lazy.stomach ); // <nothing>

As a common solution, all properties that describe the state of a particular object, like stomach above, are usually written into that object. That prevents such problems.

Eğitim haritası

Yorumlar

yorum yapmadan önce lütfen okuyun...
  • Eğer geliştirme ile alakalı bir öneriniz var ise yorum yerine github konusu gönderiniz.
  • Eğer makalede bir yeri anlamadıysanız lütfen belirtiniz.
  • Koda birkaç satır eklemek için <code> kullanınız, birkaç satır eklemek için ise <pre> kullanın. Eğer 10 satırdan fazla kod ekleyecekseniz plnkr kullanabilirsiniz)