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)
bindAllJika 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:
thissama seperti yang didapat (user.sayNowmenyebutnyauser)- Lalu berikan
...argsBound– argumen dari panggilanpartialyaitu ("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.
komentar
<code>, untuk beberapa baris – bungkus dengan tag<pre>, untuk lebih dari 10 baris – gunakan sandbox (plnkr, jsbin, < a href='http://codepen.io'>codepen…)