Saat meneruskan metode objek sebagai callback, misalnya ke setTimeout
, ada masalah umum: “kehilangan this
”.
Dalam bab ini kita akan melihat cara memperbaikinya.
Kita telah melihat contoh kehilangan this
. Ketika suatu metode diteruskan ke suatu tempat yang terpisah dari objeknya – this
akan hilang.
Inilah yang mungkin terjadi dengan setTimeout
:
biarkan pengguna = { Nama Depan: "John", ucapkan Hai() { alert(`Halo, ${ini.NamaDepan}!`); } }; setTimeout(pengguna.sayHi, 1000); // Halo, tidak terdefinisi!
Seperti yang bisa kita lihat, keluarannya tidak menunjukkan “John” sebagai this.firstName
, tetapi undefined
!
Itu karena setTimeout
mendapatkan fungsi user.sayHi
, secara terpisah dari objeknya. Baris terakhir dapat ditulis ulang menjadi:
biarkan f = pengguna.sayHi; setTimeout(f, 1000); // kehilangan konteks pengguna
Metode setTimeout
dalam browser sedikit istimewa: metode ini menyetel this=window
untuk pemanggilan fungsi (untuk Node.js, this
menjadi objek pengatur waktu, tetapi tidak terlalu penting di sini). Jadi untuk this.firstName
ia mencoba mendapatkan window.firstName
, yang tidak ada. Dalam kasus serupa lainnya, biasanya this
menjadi undefined
.
Tugasnya cukup umum – kita ingin meneruskan metode objek ke tempat lain (di sini – ke penjadwal) di mana metode tersebut akan dipanggil. Bagaimana cara memastikan bahwa judul tersebut akan dipanggil dalam konteks yang benar?
Solusi paling sederhana adalah dengan menggunakan fungsi pembungkus:
biarkan pengguna = { Nama Depan: "John", ucapkan Hai() { alert(`Halo, ${ini.NamaDepan}!`); } }; setTimeout(fungsi() { pengguna.sayHi(); // Halo, John! }, 1000);
Sekarang berfungsi, karena menerima user
dari lingkungan leksikal luar, dan kemudian memanggil metode secara normal.
Sama, tapi lebih pendek:
setTimeout(() => pengguna.sayHi(), 1000); // Halo, John!
Kelihatannya baik-baik saja, tetapi sedikit kerentanan muncul dalam struktur kode kita.
Bagaimana jika sebelum setTimeout
terpicu (ada penundaan satu detik!) user
mengubah nilai? Lalu, tiba-tiba, ia memanggil objek yang salah!
biarkan pengguna = { Nama Depan: "John", ucapkan Hai() { alert(`Halo, ${ini.NamaDepan}!`); } }; setTimeout(() => pengguna.sayHi(), 1000); // ...nilai pengguna berubah dalam 1 detik pengguna = { sayHi() { alert("Pengguna lain di setTimeout!"); } }; // Pengguna lain di setTimeout!
Solusi selanjutnya menjamin hal seperti itu tidak akan terjadi.
Fungsi menyediakan metode pengikatan bawaan yang memungkinkan untuk this
.
Sintaks dasarnya adalah:
// sintaks yang lebih kompleks akan dibahas nanti biarkan terikatFunc = func.bind(konteks);
Hasil dari func.bind(context)
adalah “objek eksotik” seperti fungsi khusus, yang dapat dipanggil sebagai fungsi dan secara transparan meneruskan panggilan ke pengaturan func
this=context
.
Dengan kata lain, boundFunc
seperti func
dengan fixed this
.
Misalnya, di sini funcUser
meneruskan panggilan ke func
dengan this=user
:
biarkan pengguna = { Nama depan: "John" }; fungsi fungsi() { alert(ini.NamaDepan); } biarkan funcUser = func.bind(pengguna); fungsiPengguna(); // Yohanes
Di sini func.bind(user)
sebagai "varian terikat" dari func
, dengan this=user
yang diperbaiki.
Semua argumen diteruskan ke func
asli “sebagaimana adanya”, misalnya:
biarkan pengguna = { Nama depan: "John" }; fungsi fungsi(frasa) { alert(frasa + ', ' + ini.NamaDepan); } // ikat ini ke pengguna biarkan funcUser = func.bind(pengguna); funcUser("Halo"); // Halo, John (argumen "Halo" diteruskan, dan ini=pengguna)
Sekarang mari kita coba dengan metode objek:
biarkan pengguna = { Nama Depan: "John", ucapkan Hai() { alert(`Halo, ${ini.NamaDepan}!`); } }; misalkan sayHi = pengguna.sayHi.bind(pengguna); // (*) // dapat menjalankannya tanpa objek sayHai(); // Halo, John! setTimeout(ucapkan Hai, 1000); // Halo, John! // meskipun nilai pengguna berubah dalam 1 detik // sayHi menggunakan nilai pra-terikat yang merujuk pada objek pengguna lama pengguna = { sayHi() { alert("Pengguna lain di setTimeout!"); } };
Di baris (*)
kita mengambil metode user.sayHi
dan mengikatnya ke user
. sayHi
adalah fungsi “terikat”, yang dapat dipanggil sendiri atau diteruskan ke setTimeout
– tidak masalah, konteksnya akan benar.
Di sini kita dapat melihat bahwa argumen diteruskan “sebagaimana adanya”, hanya saja this
diperbaiki oleh bind
:
biarkan pengguna = { Nama Depan: "John", katakan(frasa) { alert(`${phrase}, ${this.firstName}!`); } }; katakanlah = pengguna.say.bind(pengguna); say("Halo"); // Halo, John! Argumen ("Halo" diberikan untuk mengatakan) say("Sampai jumpa"); // Sampai jumpa, John! (“Sampai jumpa” diberikan untuk mengucapkan)
Metode kenyamanan: bindAll
Jika suatu objek mempunyai banyak metode dan kita berencana untuk menyebarkannya secara aktif, maka kita dapat mengikat semuanya dalam satu lingkaran:
untuk (biarkan memasukkan pengguna) { if (typeof pengguna[kunci] == 'fungsi') { pengguna[kunci] = pengguna[kunci].bind(pengguna); } }
Pustaka JavaScript juga menyediakan fungsi untuk pengikatan massal yang nyaman, misalnya _.bindAll(object, methodNames) di lodash.
Sampai saat ini kami hanya membicarakan tentang pengikatan this
. Mari kita melangkah lebih jauh.
Kita tidak hanya dapat mengikat this
, tetapi juga argumen. Hal ini jarang dilakukan, namun terkadang bisa berguna.
Sintaks lengkap dari bind
:
biarkan terikat = func.bind(konteks, [arg1], [arg2], ...);
Hal ini memungkinkan untuk mengikat konteks seperti this
dan memulai argumen fungsi.
Misalnya, kita memiliki fungsi perkalian mul(a, b)
:
fungsi mul(a, b) { kembalikan a * b; }
Mari gunakan bind
untuk membuat fungsi double
pada basisnya:
fungsi mul(a, b) { kembalikan a * b; } biarkan ganda = mul.bind(null, 2); peringatan( ganda(3) ); // = banyak(2, 3) = 6 peringatan( ganda(4) ); // = banyak(2, 4) = 8 peringatan( ganda(5) ); // = banyak(2, 5) = 10
Panggilan ke mul.bind(null, 2)
menciptakan fungsi double
baru yang meneruskan panggilan ke mul
, menetapkan null
sebagai konteks dan 2
sebagai argumen pertama. Argumen lebih lanjut disampaikan “sebagaimana adanya”.
Itu disebut penerapan fungsi parsial – kita membuat fungsi baru dengan memperbaiki beberapa parameter dari fungsi yang sudah ada.
Harap dicatat bahwa kami sebenarnya tidak menggunakan this
di sini. Tapi bind
memerlukannya, jadi kita harus memasukkan sesuatu seperti null
.
Fungsi triple
pada kode di bawah ini melipatgandakan nilainya:
fungsi mul(a, b) { kembalikan a * b; } biarkan rangkap tiga = mul.bind(null, 3); waspada( tiga kali lipat(3) ); // = banyak(3, 3) = 9 waspada( tiga kali lipat(4) ); // = banyak(3, 4) = 12 waspada(tiga kali lipat(5) ); // = banyak(3, 5) = 15
Mengapa kita biasanya membuat fungsi parsial?
Manfaatnya adalah kita dapat membuat fungsi independen dengan nama yang mudah dibaca ( double
, triple
). Kita dapat menggunakannya dan tidak memberikan argumen pertama setiap saat karena sudah diperbaiki dengan bind
.
Dalam kasus lain, penerapan parsial berguna ketika kita memiliki fungsi yang sangat umum dan menginginkan varian yang kurang universal demi kenyamanan.
Misalnya, kita memiliki fungsi send(from, to, text)
. Kemudian, di dalam objek user
kita mungkin ingin menggunakan sebagian variannya: sendTo(to, text)
yang dikirim dari pengguna saat ini.
Bagaimana jika kita ingin memperbaiki beberapa argumen, namun bukan this
? Misalnya saja untuk metode objek.
bind
asli tidak mengizinkan hal itu. Kita tidak bisa begitu saja menghilangkan konteksnya dan langsung beralih ke argumen.
Untungnya, fungsi partial
untuk hanya mengikat argumen dapat diimplementasikan dengan mudah.
Seperti ini:
fungsi parsial(fungsi, ...argsBound) { fungsi kembali(...args) { // (*) return func.call(ini, ...argsBound, ...args); } } // Penggunaan: biarkan pengguna = { Nama Depan: "John", katakan(waktu, frasa) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // menambahkan metode parsial dengan waktu tetap pengguna.sayNow = parsial(pengguna.say, Tanggal baru().getHours() + ':' + Tanggal baru().getMinutes()); pengguna.sayNow("Halo"); // Sesuatu seperti: // [10:00] John: Halo!
Hasil dari panggilan partial(func[, arg1, arg2...])
adalah wrapper (*)
yang memanggil func
dengan:
this
seperti yang didapatnya (untuk user.sayNow
panggil saja user
)
Lalu berikan ...argsBound
– argumen dari panggilan partial
( "10:00"
)
Lalu berikan ...args
– argumen yang diberikan ke pembungkusnya ( "Hello"
)
Sangat mudah untuk melakukannya dengan sintaks spread, bukan?
Juga ada implementasi _.partial siap pakai dari perpustakaan lodash.
Metode func.bind(context, ...args)
mengembalikan “varian terikat” dari fungsi func
yang memperbaiki konteks this
dan argumen pertama jika diberikan.
Biasanya kita menerapkan bind
untuk this
pada metode objek, sehingga kita bisa meneruskannya ke suatu tempat. Misalnya, untuk setTimeout
.
Saat kita memperbaiki beberapa argumen dari fungsi yang ada, fungsi yang dihasilkan (kurang universal) disebut diterapkan sebagian atau parsial .
Parsial berguna 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 sebagian dan melanjutkannya.
pentingnya: 5
Apa hasilnya?
fungsi f() { waspada( ini ); // ? } biarkan pengguna = { g: f.bind(null) }; pengguna.g();
Jawabannya: null
.
fungsi f() { waspada( ini ); // batal } biarkan pengguna = { g: f.bind(null) }; pengguna.g();
Konteks fungsi terikat sulit diperbaiki. Tidak ada cara untuk mengubahnya lebih lanjut.
Jadi meskipun kita menjalankan user.g()
, fungsi aslinya dipanggil dengan this=null
.
pentingnya: 5
Bisakah kita this
dengan pengikatan tambahan?
Apa hasilnya?
fungsi f() { alert(ini.nama); } f = f.bind( {nama: "John"} ).bind( {nama: "Ann" } ); F();
Jawabannya: Yohanes .
fungsi f() { alert(ini.nama); } f = f.bind( {nama: "John"} ).bind( {nama: "Pete"} ); F(); // Yohanes
Objek fungsi terikat eksotik yang dikembalikan oleh f.bind(...)
mengingat konteks (dan argumen jika disediakan) hanya pada waktu pembuatan.
Suatu fungsi tidak dapat diikat ulang.
pentingnya: 5
Ada nilai dalam properti suatu fungsi. Apakah akan berubah setelah bind
? Mengapa atau mengapa tidak?
fungsi ucapkan Hai() { alert( ini.nama ); } sayHi.test = 5; biarkan terikat = sayHi.bind({ nama: "Yohanes" }); waspada( terikat.uji ); // apa hasilnya? Mengapa?
Jawabannya: undefined
.
Hasil dari bind
adalah objek lain. Itu tidak memiliki properti test
.
pentingnya: 5
Panggilan ke askPassword()
pada kode di bawah ini harus memeriksa kata sandi dan kemudian memanggil user.loginOk/loginFail
tergantung pada jawabannya.
Tapi itu mengarah pada kesalahan. Mengapa?
Perbaiki baris yang disorot agar semuanya mulai berfungsi dengan benar (baris lain tidak boleh diubah).
fungsi askPassword(ok, gagal) { biarkan kata sandi = prompt("Kata sandi?", ''); if (kata sandi == "bintang rock") oke(); jika tidak, gagal(); } biarkan pengguna = { nama: 'John', masukOke() { alert(`${nama ini} masuk`); }, loginGagal() { alert(`${nama ini} gagal masuk`); }, }; askPassword(pengguna.loginOk, pengguna.loginFail);
Kesalahan terjadi karena askPassword
mendapatkan fungsi loginOk/loginFail
tanpa objek.
Ketika dipanggil, mereka secara alami berasumsi this=undefined
.
Mari kita bind
konteksnya:
fungsi askPassword(ok, gagal) { biarkan kata sandi = prompt("Kata sandi?", ''); if (kata sandi == "bintang rock") oke(); jika tidak, gagal(); } biarkan pengguna = { nama: 'John', masukOke() { alert(`${nama ini} masuk`); }, loginGagal() { alert(`${nama ini} gagal masuk`); }, }; askPassword(pengguna.loginOk.bind(pengguna), pengguna.loginFail.bind(pengguna));
Sekarang berhasil.
Solusi alternatifnya bisa berupa:
//... askPassword(() => pengguna.loginOk(), () => pengguna.loginFail());
Biasanya itu juga berfungsi dan terlihat bagus.
Ini sedikit kurang dapat diandalkan meskipun dalam situasi yang lebih kompleks di mana variabel user
mungkin berubah setelah askPassword
dipanggil, tetapi sebelum pengunjung menjawab dan memanggil () => user.loginOk()
.
pentingnya: 5
Tugasnya adalah varian yang sedikit lebih rumit dari Memperbaiki fungsi yang kehilangan "ini".
Objek user
telah diubah. Sekarang, alih-alih dua fungsi loginOk/loginFail
, ia memiliki satu fungsi user.login(true/false)
.
Apa yang harus kita lewati askPassword
pada kode di bawah ini, sehingga memanggil user.login(true)
sebagai ok
dan user.login(false)
sebagai fail
?
fungsi askPassword(ok, gagal) { biarkan kata sandi = prompt("Kata sandi?", ''); if (kata sandi == "bintang rock") oke(); jika tidak, gagal(); } biarkan pengguna = { nama: 'John', masuk(hasil) { alert( this.name + (hasil ? ' login' : ' gagal login') ); } }; tanyaPassword(?, ?); // ?
Perubahan Anda seharusnya hanya mengubah fragmen yang disorot.
Entah menggunakan fungsi pembungkus, panah menjadi ringkas:
askPassword(() => pengguna.login(benar), () => pengguna.login(salah));
Sekarang ia mendapatkan user
dari variabel luar dan menjalankannya dengan cara normal.
Atau buat sebagian fungsi dari user.login
yang menggunakan user
sebagai konteksnya dan memiliki argumen pertama yang benar:
askPassword(pengguna.login.bind(pengguna, benar), pengguna.login.bind(pengguna, salah));