Merhaba, sizlere JavaScript’te kullanılan call(), apply() ve bind() fonksiyonlarından ve hayatımızı nasıl kolaylaştırdıklarından bahsetmek istiyorum.

JavaScript, her ne kadar çok çok iyi bildiğim bir dil olmasa da benim ilgilendiğim programlama dilleri içinde en zevkli bulduğum dil. Zaten bundan daha önceki yazılarımdan birinde bu sevgimden bahsettim sanıyorum. Yazıya başlamadan önce ayrıca belirtmek isterim ki JavaScript dünyadaki en yanlış anlaşılmış programlama dili olabilir. Ülkemizde bu konuda başarılı yazılımcılar olmasına rağmen yine de JavaScript deyince malesef hala çoğu arkadaşımızın aklına gelen JQuery ile iki click olayı bağlamak ya da birşeyleri toggle etmek oluyor.

JavaScript gerçekten çok enterasan bir dil. JavaScript ile çok derli toplu projeler yapabileceğiniz gibi, işin ucunu kaçırıp karmakarışık ama yine de bir şekilde çalışan spagetti projeler de yapabilirsiniz. (Benim de ilk başlarda yaptığım gibi.) Ama unutmayın web’in ana dili JavaScript’tir. Browser ile kullanıcının etkileşimi JavaScript ile olur ve siz bir web geliştiricisi olarak JavaScript’i hakkıyla bilmezseniz basit bir işlem için bile 100’er KB’lık frameworkler yükleyip, uygulama performansı ve yükleme zamanlarıyla kullanıcılarınızı çıldırtabilirsiniz.

JavaScript, hem nesneye dayalı(OOP) bir dil, hem de değil. Aslında bu biraz da sizin JavaScript’i nasıl kullandığınıza bağlı. JavaScript ile bir çok OOP konseptini yerine getirebiliriz ancak burada nesnelerimiz class olarak değil function olarak tanımlı olmak zorundalar. Bu da JavaScript’i ilginç ve kafa karıştırıcı yapan yönlerden birisi. Eğer herhangi bir nesneye dayalı bir programlama dili biliyorsanız yazıda bahsedeceğim; miras(inheritance) ve kurucu(constructor) gibi nesneye dayalı programlama terimlerine zaten aşina olmalısınız. JavaScript’in önemli farklılıklarından biri JS fonksiyonun gövde bloğu - yani { ve } arasında kalan kısımlar - nesnemizin kurucusunu oluşturuyor. İlk olarak bunu anlamak önemli. İkinci önemli farklılık ise, JavaScript’te nesneler arasındaki miras ilişkisini prototype nesnesi ile kuruyoruz. (NOT: ECMAScript6 standardıyla module ve import gibi terimler de JS hayatımıza girecek olsalar da bu yazıyı yazdığım gün itibariyle henüz ES6 tamamlanmadı.)

Size call, apply ve bind fonksiyonlarını anlatmadan önce context ve scope(kapsam) kavramlarını anlatmak istiyorum.

Context ve Scope

JavaScript’i tam anlamıyla anlamak için scope ve context gibi kavramları anlamak gerekir. Zaten bu kavramlar iyi anlaşılmazsa başlıkta bahsettiğim call, apply ve bind fonsiyonlarını anlamak pek mümkün olmaz.

Öncelikle belirtmeliyim ki bir çok programcının zannettiği gibi context ve scope aynı şeyler değiller. Basitçe:

context => this değeri ne ise context dediğimiz de odur.

scope => bir değişkenin programın hangi kısımlarında erişilebilir olduğu ya da değişkenin yer aldığı kapsam alanı.

İki temel scope olduğunu söyleyebiliriz bunlar global scope ve yerel scope. Global scope’ta tanımladığımız bir değişken programın herhangi bir yerinden herhangi bir zaman erişilebilir durumdadır. Yerel scope ise sadece tanımlanan blok içinde etkindir. Bu blok bir döngü bloğu veya karar ifadesi bloğu olabilir (for, while, switch) ya da yerel scope’un geçerli olacağı blok, bir fonsiyonun gövde bloğu da olabilir.

var a = 1; //Global Scope
function fun1(){
  var b = 2; //Yerel Scope
  console.log('a =', a, 'b =', b); // Çıktı : a = 1 b = 2 (Hem a hem de b değişkenleri erişilebilir durumda)
}
fun1()
console.log('a =', a, 'b =', b); // Çıktı : ReferenceError: b is not defined

Gördüğünüz gibi fonksiyonun dışında b değişkenine erişmek isterseniz hata alırsınız çünkü b değişkeni, fun1() fonksiyonunun yerel scope’unda tanımlı bir yerel değişken. a ise global scope’da ve programın her yerinde kullanılabilir.

Daha da saçma bir Inception örneğiyle pekiştirmek istersek aşağıdaki gibi fonksiyonları iç içe yazabiliriz:

function fun1(){
    var a = 1;
    fun2();
    function fun2(){
        var b = 2;
        fun3();
        function fun3(){
            var c = 3;
            fun4();
            function fun4(){
                console.log(a, b, c); // Çıktı : 1 2 3
            }
        }
    }
}
fun1();

Bu örnek içinde fonksiyon içinde fonksiyonlar tanımladık ve diğer tüm fonksiyonlar fun1 içinde tanımlı oldukları için fun2, fun3 ve fun4 ‘e fun1 in scope’unda ya da kapsamında diyebiliriz. Bu da şu demek oluyor; bütün bu alt kapsam fonksiyonları ait oldukları üst kapsamın değişkenlerine erişebilirler.

Aslında burada anlatılacak o kadar çok şey var ki ben sadece call, apply ve bind fonksiyonlarını anlamak için yeterli olacağını düşündüğüm kadarına değindim.

Call ve Apply

Bu iki fonksiyonu aynı başlıkta toplamamın sebebi, ikisinin de aynı işi yapmaları ancak kullanım şekillerinde ufak bir fark olması. call ve apply her fonksiyona otomatik olarak miras edilir. Bunun sebebi JavaScript’in native kodundaki tanımlama şekilleridir.

Function.prototype.call

Function.prototype.apply

Call ve apply çağırdığımız sınıfın context’ini değiştirir ve sağladığımız argumanlarla kurucu fonksiyonu çalıştırırlar.

Örnek :

 1 function Otomobil (model, renk) {
 2   this.model = model;
 3   this.renk = renk;
 4   return this;
 5 }
 6 var bmw = function(model, renk) {
 7   Otomobil.call(this, model, renk);
 8   this.marka = "bmw";
 9 }
10 bmw.prototype = Object.create(Otomobil.prototype)
11 
12 var bmw316 = new bmw(2005, 'siyah');
13 console.log(bmw316);

Beklenen Çıktı :

bmw {model: 2005, renk: "beyaz", marka: "bmw"}

Yukarıdaki örnek JavaScript ile OOP programlamayı ve call() kullanımını anlamak için oldukça can alıcı bir örnek. Bu yüzden size satır satır olanları anlatmak istiyorum.

  1. Satır : Otomobil isimli kendini döndüren bir sınıf tasarladık. Burada dikkat etmemiz gereken, sınıf tanımımıza function tanımlayıcımız ile başlamak. Bu sayede bu sınıfın prototype’ını başka sınıflardan miras alabiliriz.

  2. Satır : bmw isimli nesne oluşturduk. Bu nesneden başka nesneler miras almayacağımız için var ile tanımlamakta sakınca görmedim.

  3. Satır : Otomobil sınıfının call fonksiyonunu this, marka, renk argümanlarıyla çağırdık. buradaki this, bmw nesnesi içinde olduğumuz için bmw context’ini işaret ediyor. model ve renk argümanları da aynı şekilde bmw sınıfının 6. satırdaki parametreleri. call fonksiyonunun çağrılması tamamlandığında, Otomobil sınıfının kurucusu model ve renk parametreleri ile çağırılıp Otomobil sınıfının context’indeki tanımlar (this.model, this.renk), bmw sınıfındaki aynı addaki alanların üzerine yazılıyor.

  4. Satır : bmw sınıfının context’inde marka özelliğini “bmw” olarak belirledik.

  5. Satır : bmw sınıfının, Otomobil sınıfının alt sınıfı olduğunu belirtlediğimiz satır. Yani Otomobil sınıfının özelliklerini bmw sınıfı tarafından miras alıyoruz.

  6. Satır : bmw sınıfından bir instance yaratıp bmw316 isimli değişkene atıyoruz.

  7. Satır : bmw316 isimli nesneyi ekrana yazdırarak kontrol ediyoruz.

    bmw {model: 2005, renk: “beyaz”, marka: “bmw”}

Toparlarsak : 8. satırda bmw sınıfının sadece marka özelliği vardı. 10. satırda Otomobil’den miras alarak model ve renk özelliklerinin de eklenmesini sağladık. 7. satırda Otomobil’i çağırıp bmw context’ini güncelledik.

Kafanızı karıştırdım mı bilmiyorum ama internette incelediğim ve kitaplarda rastladığım örneklerde kodun mantığının ne olduğunun düzgünce açıklandığında pek rastlamıyorum. O yüzden bu şekilde bir anlatım yöntemi seçtim. Bu tarz bir anlatım konusunda olumlu-olumsuz eleştirilerinizi bana iletirseniz sevinirim. Böylece bu yönteme devam edip etmeme konusunda karar verebilirim.

Alt başlığın girişinde call ve apply’ın aynı işi yaptığını sadece çağrılma yöntemlerinde ufak bir farklılık olduğundan bahsetmiştik. Fark call fonksiyonu manuel olarak tek tek parametre girilmesine karşın, apply argümanları Array olarak istiyor.

Yukarıdaki örnekte 7. satırı: Otomobil.apply(this, [model, renk]);

Olarak güncellerseniz aynı sonucu aldığınızı görürsünüz.

Call, Apply ve Anonim Fonksiyonlar

JavaScript’in en şeker özelliklerinden birisi de şüphesiz anonim fonksiyonlar. Hani şu JQuery’de sürekli kullandığınız isimsiz fonksiyonlar.

$(window).unload(function(){
   //..
 });

İşte bu tarz anonim fonksiyonlar da Function.prototype ‘dan türedikleri için call ve apply fonksiyonlarını kullanabilirsiniz.

Mozilla Developer Network’te konuyla ilgili çokça paylaşılan örneği veriyorum :

var animals = [
  {species: 'Lion', name: 'King'},
  {species: 'Whale', name: 'Fail'}
];

for (var i = 0; i < animals.length; i++) {
  (function (i) {
    this.print = function () {
      console.log('#' + i  + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

Programın işleyişini anlamak açısından gerçekten harika bir örnek. Bunu beyin cimnastiği olması için satır satır açıklamıyorum ama bu örnekle ilgili herhangi bir sorusu olan olursa seve seve cevaplarım.

Bind

Call ve Apply’yi anlatırken scope, context ve temel OOP kavramlarını da işlemiş olduk. Sırada bind() var, hayatımıza daha sonradan gelen bir özellik. Şu an anda bir çok tarayıcının kullandığı ECMAScript5 standardı ile hayatımıza girdi. Bilmiyorum hala bind() desteklemeyen tarayıcı kullanan amcalar var mıdır ama her ihtimale karşı :

if(!('bind' in Function.prototype)){
    Function.prototype.bind = function(){
        var fn = this, context = arguments[0], args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
        }
    }
}

Yukarıdaki kod ile uygulamanızın her koşulda bind() fonksiyonunu desteklemesini sağlayabilirsiniz. Aynı zamanda yukarıdaki koddan bind’ın arkaplanında yatan mantığı da anlayabiliriz.

İlk bakışta göze çarpan call ve apply de görüldüğü gibi ilk parametre olarak context alıyor diğer argümanları apply fonksiyonu uygulayarak bize bir fonksiyon döndürüyor. Call-Apply anında çalışırken bind’in bize döndürdüğü fonksiyonu sonra çağırarak context’i değiştirme şansı buluyoruz.

Bind’i uygulamadaki asıl amaç context’i değiştirmek yani farklı bir this uygulamak.

Bu sefer MSDN’den Basit bir örnek uygularsak :

// orijinal fonksiyon tanımı
var checkNumericRange = function (value) {
    if (typeof value !== 'number')
        return false;
    else
        return value >= this.minimum && value <= this.maximum;
}

// Callback fonksiyonunda 'this' olacak nesne
var range = { minimum: 10, maximum: 20 };

// checkNumericRange fonksiyonunu bind ile bağla
var boundCheckNumericRange = checkNumericRange.bind(range);

// Yeni oluşturduğumuz fonksiyonu 12 sayısı istediğimiz aralıkta mı diye kontrol için kullanalım.
var result = boundCheckNumericRange (12);
console.log(result);

// Çıktı: true

Yukarıdaki örnekte checkNumericRange fonksiyonunun context’i için this.minimum ve this.maximum özellikleri tanımlanmamış dolayısıyla dışarıdan bir context yapıştırmamız gerekiyor. range nesnesi minimum ve maximum alanlarına sahip. var boundCheckNumericRange = checkNumericRange.bind(range); bu kod ile range nesnesini checkNumericRange fonksiyonuna context olarak yapıştırdık ve yeni bir değişkene fonksiyon tipinde atadık. Ve fonksiyonu uygun parametre ile çağırdığımızda sonuç karşımızda. :)

Yazım ve kod hatalırını bulursanız bana mutlaka bildirmenizi rica ediyorum.

Bir sonraki blog yazımda görüşmek üzere hoşçakalın.