24 Ocak 2022

Metodlar ve prototipler

Bu bölümde prototipler ile çalışmak için ek metodlardan bahsedeceğiz

Bizim bildiğimizin haricinde prototipi ayarlamak ve almak için başka yöntemler de bulunmaktadır:

Örneğin:

let animal = {
  eats: true
};

// animal prototipi ile yeni bir rabbit objesi yaratma.
let rabbit = Object.create(animal);

alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // rabbit'in prototipini alma

Object.setPrototypeOf(rabbit, {}); // rabbit'in prototipini {}'e çevirme.

Object.create opsiyonel olarak ikinci bir argümana sahiptir: özellik tanımlayıcı. Aşağıdaki gibi yeni objeye özellikler ekleyebiliriz:

let animal = {
  eats: true
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true
  }
});

alert(rabbit.jumps); // true

Tanımlayıcıların Özellik bayrakları ve tanımlayıcılar bölümünde üstünden geçilmiştir. Formatları aynıdır. Object.create kullanarak obje klonlama işlemini yapabiliriz. Bu objenin özelliklerini for..in ile dolanmaktan daha etkin bir yöntemdir.

// Objenin yüzeysel klonu
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

Bu tam olarak obj’nin aynısını verir. Tüm özellikler: dönülebilir veya dönülemez, veri özellikleri, alıcı ve ayarlayıcılar – her şey, ayrıca doğru [[Prototype]] ile

Tarihçe

[[Prototype]]'ı ayarlayabileceğimiz yöntemleri saymaya kalsak baya zorluk yaşarız. Çok fazla yöntem bulunmaktadır.

Neden?

Birçok nedeni bulunmaktadır.

  • Yapıcının "prototype" özelliği ilk javascript ortaya çıktığından beri bulunmaktadır.
  • 2012’de: Object.create bir standart olarak oturdu. Bu verilen prototip ile objeleri yaratmaya izin verdi. Fakat bunları almaya veya ayarlamaya değil. Bundan dolayı tarayıcılar bir standart olmayan __proto__ erişimcilerini uygulayarak alıcı ve ayarlayıcılara ( get/set)'e izin verdi.
  • 2015’te: Object.setPrototypeOf ve Object.getPrototypeOf bir standart olarak eklendi. __proto__ defakto şeklinde aslında her yerde kullanılmıştı, Bundan dolayı çokta kulllanılan özellikler olmadı, sadece tarayıcı harici çevrelerde kullanılır oldu.

Artık bunların hepsi bizim kullanımımızdadır.

Teknik olarak [[Prototype]]'ı istediğimiz an alma/ayarlama işi yapabiliriz. Fakat genelde bunu sadece obje yaratırken kullanır ve daha sonra düzenleme yapmayız: rabbit, animaldan kalıtım alır fakat onu değiştirmez. JavaScript motorları da bunu yüksek derecede optimize edebilir. Prototipi Object.setPrototypeOf veya obj.__proto__ ile sonradan değiştirmek oldukça yavaş bir operasyondur. Ama mümkündür.

“En basit” Objeler

Bildiğiniz gibi objeler anahtar/değer ikilisini tutan ilişkisel dizi şeklinde kullanılabilir.

…Eğer içerisinde kullanıcı-kaynaklı anahtarlar ( örneğin kullanıcının girdiği sözlük tipi veriler) var ise, ilginç bir aksaklık meydana gelir: "__proto_" haricinde tüm anahtarlar doğru çalışır.

Örneğin:

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object] olarak döner ,  "some value" değil!

Eğer kullanıcı tipleri __proto__ içerisinde ise atama görmezden gelinir!

Bu aslında çok da sürpriz olmasa gerek. __proto__ farklı bir özelliktir: Ya obje olur veya null, mesela bir karakter dizisi prototip olamaz.

Fakat buradaki amacımız böyle bir davranışı uygulamak değildi, değil mi? Biz key/value ikililerini kaydetmekti. "__proto__" anahtarı düzgün bir şekilde kaydedilmedi. Bundan dolayı bu bir bugdır. Burada etkisi berbat değildir fakat diğer durumlarda prototip gerçekten değişebilir ve çalışma tamamen istenmeyen bir sonuca varabilir.

Daha kötüsü – genelde geliştiriciler böyle bir ihtimali düşünmezler bile. Bundan dolayı böyle bir bug’lar fark edilebilir ve saldırıya açık hale gelirler, özellikle JavaScript server tarafında kullanıldıysa.

Böyle bir olay sadece __proto__'da meydana gelir diğer tüm özellikler normalde “atanabilir”'dir.

Bu problemden nasıl kaçınılabilir?

Öncelikle Map kullanılabilir, her şey doğru çalışır.

Fakat burada bize Obje yardımcı olabilir, çünkü dili yaratıcılar bu konuları uzun zaman önce düşünmüşler.

__proto__ objenin bir özelliği değildir. Fakat Object.prototype’a erişim sağlar (accessor):

Bundan dolayı, Eğer obj.__proto__ okunur veya atanırsa, ilgili alıcı/ayarlayıcı prototipten çağırılır, böylece [[Prototoy]] alınır/ayarlanır.

Başlangıçta __proto__ , [[Prototype]]a erişmek için bir yol olarak tanımlanmıştır, [[Prototype]] değil.

Eğer, eğer objeyi ilişkisel dizi olarak kullanmak istiyorsanız şu şekilde yapabilirsiniz:

let obj = Object.create(null);

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

Object.create(null) prototip’i olmayan boş bir obje yaratır.([[Prototype]] null’dur):

Bundan dolayı __proto__ için atadan kalan alıcı/ayarlayıcı bulunmamaktadır. Artık sıradan bir veri özelliği olarak işlenir, bundan dolayı yukarıdaki örnek doğru bir şekilde çalışır.

Böyle objelere “en basit” veya “saf sözlük objeleri” denir, Çünkü bunlar sıradan objelerden {...} bile daha basittirler.

Bu objelerin kötü tarafı ise, içinde hiçbir varsayılan metod bulunmaz, Örneğin: toString:

let obj = Object.create(null);

alert(obj); // Error (no toString)

…Fakat bu genelce ilişkili diziler için problem oluşturmaz.

Çoğu obje-bağlantılı metodlar Object.something(...) şeklindedir ve Object.keys(obj) gibi olduğundan prototipte yer almazlar, bundan dolayı şu şekilde çalışmaya devam ederler:

let chineseDictionary = Object.create(null);
chineseDictionary.hello = "ni hao";
chineseDictionary.bye = "zai jian";

alert(Object.keys(chineseDictionary)); // hello,bye

Tüm özellikleri alma

Bir objeden Anahtar/değer ikilisini almak için birçok yol bulunmaktadır.

Aşağıdaki kullanımını zaten biliyorsunuz:

Eğer sembolik özellikler istenirse:

Eğer döngülenemez özellikleri istenirse:

Eğer tüm özellikler istenir ise:

Bu metodlar hangi özellikleri döndürecekleri hususunda farklılıkları olsa da hepsi objenin kendi üzerinde çalışır. Prototipte bulunan özellikler listelenmez.

for..in döngüsü ise biraz farklıdır: Kalıtılmış özellikler’i de döngüye alır.

Örneğin:

let animal = {
  eats: true
};

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

// Sadece kendi anahtarları
alert(Object.keys(rabbit)); // jumps

// kalıtılmış anahtarları da içerir.
for(let prop in rabbit) alert(prop); // jumps, sonra  eats

Eğer kalıtılmış özellikler ayrıştırılmak istenirse bunun için varolan obj.hasOwnProperty(key) kullanılabilir: Eğer obj key adında kalıtımsal olmayan bir özelliğe sahipse true dönderir.

Kalıtımsal özellikleri bu şekilde filtreleyebilir veya başka bir şey yapabiliriz:

let animal = {
  eats: true
};

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

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);
  alert(`${prop}: ${isOwn}`); // jumps:true, then eats:false
}

Şu şekilde kalıtım zinciri oluşmuştur: rabbit, sonra animal ve en son Object.prototype ( çünkü animal tam objedir {…} ), sonra bunun üzerine null:

"/article/prototype-methods/rabbit-animal-object.svg" görseli bulunamadı

Zincire bakarsanız rabbit.hasOwnProperty nereden geliyor görebilirsiniz. Object.prototype.hasOwnPropery, diğer bir deyişle kalıtılmış.

… Fakat öyleyse neden for..in içerisinde görünmüyor. Halbuki for..in ile kalıtılmış tüm özelliklerin listeleneceğini söylemiştik. Aslında cevap basit, bu döngüye alınamaz. Tıpkı diğer Object.prototype’larda olduğu gibi. Bundan dolayı listelenmemiştir.

Özet

Bu bölümde anlatılanların üzerinden kısaca geçecek olursak:

Şunu da belirtmiş olalım __proto__ [[Prototype]] için alıcı/ayarlayıcıdır. Bu Object.prototype içerisinde yer alır, tıpkı diğer metodlar gibi.

Prototip olmadan bir objeyi Object.create(null) şeklinde yaratmak mümkündür. Bu tür objeler “saf dictionary yapısı” olarak kullanılır. "__proto__" anahtarı ile bir problemi bulunmamaktadır.

Obje özelliklerini döndüren ( Object.keys ve diğerleri ) – “kendi” özelliklerini döndürür. Eğer kalıtılmış olanlarını da istersek for..in kullanabiliriz.

Görevler

önem: 5

Object.create(null) olarak yaratılan ve her türlü anahtar/değer ikilisini tutan dictionary adında bir obje bulunmaktadır.

Buna dictionary.toString() metodu ekleyin, bu anahtarların virgül ile ayrılmış halini dönsün. Ama toString metodu for..in ile objenin keylerini dönerken görünmemelidir.

Şu şekilde çalışmalıdır:

let dictionary = Object.create(null);

//  dictionary.toString metodunu ekleyeceğiniz yer

// biraz veri ekleyin
dictionary.apple = "Apple";
dictionary.__proto__ = "test"; // __proto__ burada normal özellik olarak kullanılmıştır

// sadece apple ve __proto__ yazmalı
for(let key in dictionary) {
  alert(key); // "apple", sonra "__proto__"
}

// Artık sizin yazacağınız toString metodu burada çalışmalıdır.
alert(dictionary); // "apple,__proto__"

Metod tüm dönülebilir anahtarlını Object.keys ile alır ve listesini döner.

toString’i dönülemez yapmak için, özellik tanımlayıcı ile tanımlamak gereklidir. Bunun yazımı Object.create ile olur ve bu ikinci argüman olarak özellik tanımlayıcı alır.

let dictionary = Object.create(null, {
  toString: { // toString özelliğini tanımla.
    value() { // Değeri bir fonksiyondur.
      return Object.keys(this).join();
    }
  }
});

dictionary.apple = "Apple";
dictionary.__proto__ = "test";

// apple ve __proto__ döngüde yer alır
for(let key in dictionary) {
  alert(key); // "apple", sonra "__proto__"
}

// listenin virgül ile ayrılmış versiyonu döner.
alert(dictionary); // "apple,__proto__"

Tanımlayıcı ile özellik yarattığımızda bunun bayrakları varsayılan olarak false olur. Bundan dolayı yukarıdaki dictionary.toString dönülemezdir.

Daha fazla bilgi için Özellik bayrakları ve tanımlayıcılar bölümünü inceleyebilirsiniz.

önem: 5

Yeni bir rabbit objesi yaratalım:

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert(this.name);
};

let rabbit = new Rabbit("Rabbit");

Aşağıdaki çağrılar aynı şeyleri yapar mı yapmaz mı?

rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();

İlk çağrı this == rabbit’e sonrakiler ise this eşittir Rabbit.prototype'a olacak çekilde tanımlanmıştır. Daha önce de bahsettiğimiz gibi asıl objenokta`'dan önceki bölümdür.

Bundan dolayı sadece ilk çağrı Rabbit’i gösterir. Diğerleri ise undefined’dır.

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert( this.name );
}

let rabbit = new Rabbit("Rabbit");

rabbit.sayHi();                        // Rabbit
Rabbit.prototype.sayHi();              // undefined
Object.getPrototypeOf(rabbit).sayHi(); // undefined
rabbit.__proto__.sayHi();              // undefined
Eğitim haritası