4 Ekim 2022

Fonksiyon bağlama

Obje metodları ile setTimeout kullanıldığında veya obje metodları iletilirken, this'in kaybolması bilinen bir problemdir.

Aniden, this kaybolur. Bu problem başlangıç seviyesi geliştiriciler için çok tipiktir, bazen deneyimli geliştiriciler de bu hataya düşerler.

"this"i kaybetme

JavaScript’te this in ne kadar kolay bir şekilde kaybolduğunu zaten biliyorsunuz. Eğer bir metod objeden farklı bir yere iletilirse this kaybolur.

Bu setTimeout ile nasıl olur bakalım:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(user.sayHi, 1000); // Hello, undefined!

Gördüğünüz gibi, çıktı "John"u göstermedi bunun yerine undefined döndü!

Bunun nedeni setTimeout'un user.sayHi fonksiyonunun objeden ayrı olmasıdır. Son satır şu şekilde yazılabilir:

let f = user.sayHi;
setTimeout(f, 1000); // kullanıcı kaynağı kayboldu

Tarayıcıda setTimeout kullanımı biraz özeldir: this=window olarak ayarlanır. ( Node.JS için this timer objesi olur, fakat burada pek de önemli değil.) Öyleyse this.firstName bu değeri window.firstName'den almaya çalışır, fakat böyle bir şey yok. Buna benzer durumlarda siz de göreceksiniz this genelde undefined olur.

Aslında yapmak istediğimiz çok basit obje metodunu çağırılan yere ( – ) iletmek istiyoruz ( burada – zamanlayıcıdır). Bunun doğru kaynakta çağırıldığına nasıl emin olunabilir?

Çözüm 1: saklayıcı

En basit çözüm bir saklayıcı ( wrapper ) fonksiyonu kullanmaktır:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000);

Çalışmasının nedeni user'ı dış sözcük ortamından almasıdır, sonrasında metodu normal bir şekilde çalıştırır.

Aynısı, fakat biraz daha kısa hali:

setTimeout(() => user.sayHi(), 1000); // Hello, John!

Fena değil, fakat kod yapısal olarak biraz sorunlu görünüyor.

setTimeout çalışmadan önce ( 1 sn ara ile çalışıyor ) user değeri değişirse? Sonra aniden yanlış objeyi çağıracaktır.

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// ...kullanıcının değeri 1 saniye içinde değişir
user = { sayHi() { alert("Another user in setTimeout!"); } };

//SetTimeout'da başka bir kullanıcı!

Bir sonraki çözüm içe böyle bir şeyin olmasını engeller.

Çözüm 2: bağlama

Fonksiyonlar bind varsayılan fonksiyonu sağlarlar. Bu fonksiyon this'in sabitlenmesini olanak verir.

Basitçe yazımı şu şekildedir:

// daha karmaşık yazımlarına ileride geleceğiz.
let boundFunc = func.bind(kaynak);

func.bind(kaynak)'ın sonucu özel bir fonksiyon benzeri "egzotik obje"dir. Fonksiyon gibi çağırılabilir ve saydam bir şekilde çağrıyı func'a this=kaynak olacak şekilde iletir.

Diğer bir deyişle boundFunc aslında sabit this'e sahip func'dur.

Örneğin burada funcUser çağrıyı func fonksiyonuna this=user olacak şekilde iletir.

let user = {
  firstName: "John"
};

function func() {
  alert(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // John

Burada func.bind(user) aslında func'un this=user olarak “bağlanmış halidir”.

Tüm argümanlar orjinal func'a olduğu gibi aktarılır, örneğin:

let user = {
  firstName: "John"
};

function func(phrase) {
  alert(phrase + ', ' + this.firstName);
}

// this'i user'a bağla.
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John ("Hello" iletildi ve this=user oldu)

Bunu obje metodu ile deneyecek olursak:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user); // (*)

sayHi(); // Hello, John!

setTimeout(sayHi, 1000); // Hello, John!

(*) satırında user.sayHi metodunu alıyoruz ve user'a bağlıyoruz. sayHi bu durumda bağlanmış fonksiyon oluyor. Böylece tek başına çağırılabilir veya setTimeout içerisinde çağırılabilir. Nereden çağırıldığı çok da önemli değildir. Kaynağı her zaman doğru olacaktır.

Gördüğünüz gibi tüm argümanlar “olduğu gibi” iletilir, sadece this bind tarafından sabitlenmiştir:

let user = {
  firstName: "John",
  say(phrase) {
    alert(`${phrase}, ${this.firstName}!`);
  }
};

let say = user.say.bind(user);

say("Hello"); // Hello, John ("Hello" `say` fonksiyonuna iletildi)
say("Bye"); // Bye, John ("Bye" `say` fonksiyonuna iletildi.)
Kullanışlı metod: bindAll

Eğer bir objenin birçok metodu var ise bunu aktif olarak gerekli yerlere iletip, bunları bir döngü içerisine alabiliriz:

for (let key in user) {
  if (typeof user[key] == 'function') {
    user[key] = user[key].bind(user);
  }
}

Bu şekilde büyük bağlama olayları için bazı JavaScript kütüphanelerinden yardım alınabilir. Örneğin lodash ,_.bindAll(obj) fonksiyonuna sahiptir.

Özet

func.bind(kaynak, ...args) func fonksiyonunun "bağlanmış hali"ni döndürür. Bu bağlanmış halde this ve argümanlar sabitlenir.

Bind genelde obje metodlarındaki this'in sabitlenmesi amacıyla kullanılır, sonrasında istenilen yere iletilebilir. setTimeout örneği gibi. bind'in modern geliştirmede kullanılmasının birçok nedeni vardır bunlara ilerleyen konularda değineceğiz.

Görevler

önem: 5

Aşağıdaki kodun çıktısı nedir?

function f() {
  alert( this ); // ?
}

let user = {
  g: f.bind(null)
};

user.g();

Cevap: null.

function f() {
  alert( this ); // null
}

let user = {
  g: f.bind(null)
};

user.g();

Bağlanan fonksiyona doğrudan değer atanmıştır. Bundan dolayı değiştirilemez. Bundan dolayı user.g() yi çağırsanız bile orjinal fonksiyon this=null şeklinde çağırılacaktır.

önem: 5

Fazladan bir defa daha bağlama işlemi yaparak this'i değiştirebilir miyiz?

Aşağıdaki kodun çıktısı nedir?

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Ann" } );

f();

Cevap: John.

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Pete"} );

f(); // John

f.bind(...) tarafından dönen egzotik objeler bound function yaratıldığı zamanki kaynağı hatırlar. Bundan dolayı bir fonksiyon birden fazla defa bağlanamaz.

önem: 5

Fonksiyonun özelliğinde bir değer var. Bu değer bind edildikten sonra değişir mi? Neden?

function sayHi() {
  alert( this.name );
}
sayHi.test = 5;

let bound = sayHi.bind({
  name: "John"
});

alert( bound.test ); // çıktısı ne olacak? neden?

Cevap: undefined.

bind'ın sonucu başka bir objedir. Bu objenin test özelliği bulunmamaktadır.

önem: 5

askPassword() çağrısı şifreyi kontrol etmeli ve buna göre user.loginOk veya user.loginFail'i çağırmalıdır.

Fakat bu bir hataya neden oluyor. Neden?

Sadece üstü çizili satırda değişiklik yaparak her şeyin doğru çalışmasını sağlayınız. ( diğer satırlarda değişiklik yapılmamalıdır)

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  loginOk() {
    alert(`${this.name} logged in`);
  },

  loginFail() {
    alert(`${this.name} failed to log in`);
  },

};

askPassword(user.loginOk, user.loginFail);

Nedeni ask'ın loginOk/loginFail fonksiyonlarını obje olmadan almasıdır.

Bunları çağırdığında doğal olarak this=undefined olarak çalışacaktır.

Kaynağı bağlar isek:

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  loginOk() {
    alert(`${this.name} logged in`);
  },

  loginFail() {
    alert(`${this.name} failed to log in`);
  },

};

askPassword(user.loginOk.bind(user), user.loginFail.bind(user));

Şimdi çalışacaktır.

Alternatif çözüm şu şekilde olabilir:

//...
askPassword(() => user.loginOk(), () => user.loginFail());

Genelde bu da çalışır. Fakat daha karmaşık durumlarda user'ın soru ve () => user.loginOk() arasında üzerine yazılabilir.

Eğitim haritası