16 Desember 2020

Function binding

Ketika mengirimkan metode objek sebagai callback, seperti setTimeout, terdapat sebuah masalah: "kehilangan this".

Didalam chapter ini kita akan belajar cara memperbaikinya.

Kehilangan ???this???

Kita sudah melihat beberapa contoh saat kehilangan this. Sekalinya sebuah metode dikirim kebagian kode lain dengan terpisah dari objeknya ??? this akan menghilang dari metodenya.

Ini adalah bagaimana hal itu terjadi dengan setTimeout:

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

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

Seperti yang bisa kita lihat, keluarannya tidak menampilkan ???John??? sebagai this.firstName, tapi menampilkan undefined!

Itu karena setTimeout mendapatkan fungsi user.sayHi, terpisah dari objeknya. Baris terakhir bisa ditulis ulang sebagai:

let f = user.sayHi;
setTimeout(f, 1000); // kehilangan konteks dari user

Metode setTimeout didalam peramban sedikit spesial: metode tersebut menyetel this=window untuk pemanggilan fungsi (untuk Node.js, this menjadi objek timer, tapi tidak terlalu penting disini). Jadi untuk this.firstName metodenya jadi mendapatkan window.firstName, yang mana tidak ada. Dalam kasus serupa lainnya this akan menjadi undefined.

Tugasnya cukup tipikal ??? kita ingin mengirim metode objek ke bagian kode lainnya (disini ??? kepada penjadwal/setTimeout) dimana metodenya akan dipanggil. Bagaimana cara untuk memeriksa konteksnya dipanggil dengan benar?

Solusi 1: pembungkus

Solusi sederhananya adalah untuk menggunakan fungsi pembungkus:

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

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

Kode diatas bekerja, karena user didapatkan dari lingkungan leksikal terluar, dan lalu memanggil metodenya secara normal.

Solusi yang sama, tapi lebih pendek:

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

Terlihat bagus, tapi sedikit memiliki kerentanan yang akan muncul pada struktur kodenya.

Bagaimana jika sebelum setTimeout berjalan (terdapat penundaan selama satu detik!) nilai user untuk berubah? Maka, tiba-tiba,fungsinya akan memanggil objek yang salah.

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

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

// ...nilai dari user berubah sebelum 1 detik!
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

// setTimeout menggunakan user yang berbeda!

Solusi selanjutnya akan menjamin hal seperti diatas tidak akan terjadi.

Solusi 2: bind

Fungsi menyediakan sebuah metode bawaan bind yang mengijinkan untuk membernarkan this.

Sintaks dasarnya adalah:

// contoh sintaks yang lebih kompleks akan kita segera lihat
let boundFunc = func.bind(context);

hasil dari func.bind(contenxt) adalah sesuatu yang terlihat seperti fungsi spesial atau bisa disebut dengan ???objek eksotik???, yang dapat dipanggil sebagai fungsi dan dapat melanjutkan pemanggilan kepada func sambil menyetel this=context.

Dengan kata lain, memanggil boundFunc sama seperti func dengan nilai this yang tetap.

Contoh, disini funcUser mengirimkan sebuah panggilan kepada func dengan this=user:

let user = {
  firstName: "John"
};

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

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

Disini func.bin(user) sebagai sebuah varian dari func, dengan nilai tetap this=user.

Seluruh argumen dikirim kepada func asli ???sebagaimana adanya???, contoh:

let user = {
  firstName: "John"
};

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

// bind this to user
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John (argumen "Hello" dikirim, dan this=user)

Sekarang kita coba dengan menggunakan metode objek:

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

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

// bisa dijalankan tanpa objek
sayHi(); // Hello, John!

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

// bahkan jika nilai dari user berubah sebelum 1 detik
// sayHi menggunakan nilai yang telah diikat, yang mana telah mereferensi kepada objek yang lama
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

Didalam baris (*) kita menggunakan metode user.sayHi dan mengikatkannta kepada user. sayHi adalah sebuah fungsi ???terikat???, yang bisa dipanggil sendiri atau dikirimkan kepada setTimeout ??? itu tidaklah penting, yang penting adalah konteksnya tepat.

Disini kita bisa melihat argumen yang dikirimkan ???seperti adanya???, hanya saja this nilainya menjadi tetap oleh bind:

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

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

say("Hello"); // Hello, John (argumen "Hello" dikirim untuk digunakan)
say("Bye"); // Bye, John ("Bye" dikirim untuk digunakan)
Metode yang bermanfaat: bindAll

Jika sebuah objek mempunyai beberapa metode dan kita berencana untuk mengirimkannya kebagian kode lain secara terus-menerus, kita bisa mengikatkannya didalam sebuah perulangan:

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

Librari Javascript juga menyediakan fungsi untuk memudahkan pengikatan/binding masal, contoh _.bindAll(object, methodNames) didalam lodash.

Partial functions/Fungsi sebagian

Sampai sekarang kita hanya berbicara tentang binding/pengikatan this. Ayo kita lihat lebih dalam.

Kita bisa mengikat bukan hanya this, tapi juga argumen. Yang mana sangat jarang digunakan, tapi terkadang cukup mudah digunakan.

Sintaks penuh dari bind:

let bound = func.bind(context, [arg1], [arg2], ...);

Yang mana mengijinkan kita untuk mengikat konteks sebagai this dan memulai argumen dari sebuah fungsi.

Contoh, kita mempunyai sebuah fungsi perkalian mul(a, b):

function mul(a, b) {
  return a * b;
}

Kita gunakan bind untuk membuat sebuah fungsi double didalamnya:

function mul(a, b) {
  return a * b;
}

let double = mul.bind(null, 2);

alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

Panggilan pada mul.bind(null, 2) membuat function double baru yang memberikan panggilan terhadap mul, memperbaiki null sebagai konteksnya dan 2 sebagai argumen pertamanya. Argumen-argumen lebih lanjut yang diberikan ???as is/sebagaimana adanya???.

Itu dipanggil partial function application ??? kita membuat sebuah fungsi baru dengan memperbaiki beberapa parameter dari yang sudah ada.

Harap dicatat bahwa disini kita tidak menggunakan this. Tapi bind memerlukannya, jadi kita harus meletakkan di dalam sesuatu seperti null.

Fungsi triple di dalam kode dibawah ini melipatkan tiga kali lipat nilai tersebut:

function mul(a, b) {
  return a * b;
}

let triple = mul.bind(null, 3);

alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15

Kenapa kita umumnya membuat fungsi parsial?

Manfaatnya bahwa kita dapat membuat sebuah fungsi independen dengan nama yang dapat dibaca (double, triple). Kita bisa menggunakannya dan tidak menyediakan argumen pertamanya setiap saat karena sudah diperbaiki dengan bind.

Dalam kasus lain, aplikasi parsial berguna saat kita punya sebuah fungsi generik dan menginginkan varian yang kurang universal untuk kenyamanan.

Contoh, kita punya sebuah fungsi send(from, to, text). Kemudian, di dalam objek user kita mungkin ingin menggunakan varian parsial darinya: sendTo(to, text) yang dikirim dari user saat ini.

Menjadi parsial tanpa konteks

Bagaimana jika kita ingin memperbaiki beberapa argumen, tetapi bukan konteks this? Contoh, untuk sebuah method objek.

bind yang asli tidak mengizinkan itu. Kita tidak bisa begitu saja mengabaikan konteks dan lompat ke argumen.

Untungnya, fungsi partial untuk mengikat argumen saja dapat dengan mudah diterapkan.

Seperti ini:

function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

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

// tambahkan method parsial dengan waktu tetap
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!

Hasil dari panggilan partial(func[, arg1, arg2...]) yaitu sebuah pembungkus (*) yang memanggil func dengan:

  • this sama seperti yang didapat (user.sayNow menyebutnya user)
  • Lalu berikan ...argsBound ??? argumen dari panggilan partial yaitu ("10:00")
  • Lalu berikan ...args ??? argumen yang diberikan ke pembungkus (" Hello ")

Sangat mudah melakukannya dengan sintaks penyebaran, bukan?

Juga ada implementasi _.partial yang siap dari perpustakaan lodash.

Kesimpulan

Method func.bind(context, ...args) mengembalikan sebuah ???varian terikat??? dari function func yang memperbaiki konteksthis dan argumen pertama jika diberikan.

Biasanya kita menerapkan bind untuk memperbaiki this untuk sebuah method objek, sehingga kita bisa memberikannya ke suatu tempat. Misalnya, ke setTimeout.

Ketika kita memperbaiki beberapa argumen dari function yang ada, fungsi yang dihasilkan (less universal) disebut partially applied atau partial.

Parsial lebih mudah digunakan ketika kita tidak ingin mengulangi argumen yang sama berulang kali. Seperti jika kita memiliki fungsi send (from, to), dan from harus selalu sama untuk tugas kita, kita bisa mendapatkan sebuah partial dan melanjutkannya.

Tugas

Apakah keluarannya?

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

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

user.g();

Jawabannya: null.

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

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

user.g();

Konteks dari pengikatan fungsi sangat sulit diperbaiki. Tidak ada cara untuk merubahnya dilain waktu.

Jadi bahkan ketika kita menjalankan user.g(), fungsi aslinya dipanggil dengan this=null.

Bisakah kita merubahthis dengan pengikatan tambahan?

Apakah yang akan menjadi keluarannya?

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

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

f();

Jawabannya: John.

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

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

f(); // John

Objek eksotis bound function yang dikembalikan oleh f.bind(...) mengingat konteksnya (dan argumen jika ada) hanya pada waktu pembuatan.

Sebuah fungsi tidak bisa diikat-ulang.

Terdapat sebuah nilai didalam properti dari sebuah fungsi. Apakah properti tersebut akan berubah setelah bind? Kenapa, atau kenapa tidak?

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

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

alert( bound.test ); // apakah keluarannya? kenapa?

Jawabannya: undefined.

Hasil dari bind adalah objek lainnya. Objek tersebut tidak memiliki properti test.

Pemanggilan kepada askPassword() didalam kode dibawah harus memeriksa passwordnya dan lalu memanggil user.loginOk/loginFail tergantung dari jawabannya.

Tapi pemanggilan itu mengembalikan sebuah error. kenapa?

Perbaiki baris yang di tandai agar kodenya dapat berjalan dengan benar (baris lainnya tidak perlu diubah).

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);

Errornya muncul karena ask mendapatkan fungsi loginOk/loginFail tanpa objeknya.

ketika ask memanggil, loginOk/loginFail mengasumsikan bahwa this=undefined.

Ayo kita coba bind konteksnya:

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));

Sekarang kodenya berjalan.

Alternatif lainnya bisa menggunakan:

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

Contoh diatas biasanya bekerja dan terlihat lebih rapih.

Cara ini sedikit kurang bisa digunakan didalam situasi yang lebih kompleks dimana variabel user mungkin berubah setelah askPassword dipanggil, tapi sebelum pengguna menjawab dan memanggil () => user.loginOk().

Tugas yang ini adalah varian yang sedikit lebih sulit daripada Perbaiki sebuah fungsi yang telah kehilangan "this".

Objek usernya telah dimodifikasi. Sekarang daripada menggunakan dua fungsi loginOk/loginFail, sekarang hanya memiliki satu fungsi user.login(true/false).

Apa yang harus kita kirimkan kedalam askPassword di kode dibawah, apakah harus memanggul user.login(true) sebagai ok dan user.login(false) sebagai fail?

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

let user = {
  name: 'John',

  login(result) {
    alert( this.name + (result ? ' logged in' : ' failed to log in') );
  }
};

askPassword(?, ?); // ?

Ubahlah kodenya hanya pada bagian yang ditandai.

  1. Funakan fungsi pembungkus, lebih jelasnya gunakanlah fungsi arrow:

    askPassword(() => user.login(true), () => user.login(false));

    Sekarang fungsi `user dapat diambil dari variabel luar dan dijalankan dengan normal.

  2. Atau buat sebuah fungsi mini dari user.login yang menggunakan user sebagai konteksnya dan yang mana mempunyai argumen pertama yang benar:

    askPassword(user.login.bind(user, true), user.login.bind(user, false));
Peta tutorial